diff src/org/eclipse/jetty/io/nio/SslConnection.java @ 802:3428c60d7cfc

replace jetty jars with source
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 07 Sep 2016 21:15:48 -0600
parents
children 8e9db0bbf4f9
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/eclipse/jetty/io/nio/SslConnection.java	Wed Sep 07 21:15:48 2016 -0600
@@ -0,0 +1,865 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.io.nio;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLEngineResult.HandshakeStatus;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+
+import org.eclipse.jetty.io.AbstractConnection;
+import org.eclipse.jetty.io.AsyncEndPoint;
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.Connection;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+import org.eclipse.jetty.util.thread.Timeout.Task;
+
+/* ------------------------------------------------------------ */
+/** SSL Connection.
+ * An AysyncConnection that acts as an interceptor between and EndPoint and another
+ * Connection, that implements TLS encryption using an {@link SSLEngine}.
+ * <p>
+ * The connector uses an {@link AsyncEndPoint} (like {@link SelectChannelEndPoint}) as
+ * it's source/sink of encrypted data.   It then provides {@link #getSslEndPoint()} to
+ * expose a source/sink of unencrypted data to another connection (eg HttpConnection).
+ */
+public class SslConnection extends AbstractConnection implements AsyncConnection
+{
+    private final Logger _logger = Log.getLogger("org.eclipse.jetty.io.nio.ssl");
+
+    private static final NIOBuffer __ZERO_BUFFER=new IndirectNIOBuffer(0);
+
+    private static final ThreadLocal<SslBuffers> __buffers = new ThreadLocal<SslBuffers>();
+    private final SSLEngine _engine;
+    private final SSLSession _session;
+    private AsyncConnection _connection;
+    private final SslEndPoint _sslEndPoint;
+    private int _allocations;
+    private SslBuffers _buffers;
+    private NIOBuffer _inbound;
+    private NIOBuffer _unwrapBuf;
+    private NIOBuffer _outbound;
+    private AsyncEndPoint _aEndp;
+    private boolean _allowRenegotiate=true;
+    private boolean _handshook;
+    private boolean _ishut;
+    private boolean _oshut;
+    private final AtomicBoolean _progressed = new AtomicBoolean();
+
+    /* ------------------------------------------------------------ */
+    /* this is a half baked buffer pool
+     */
+    private static class SslBuffers
+    {
+        final NIOBuffer _in;
+        final NIOBuffer _out;
+        final NIOBuffer _unwrap;
+
+        SslBuffers(int packetSize, int appSize)
+        {
+            _in=new IndirectNIOBuffer(packetSize);
+            _out=new IndirectNIOBuffer(packetSize);
+            _unwrap=new IndirectNIOBuffer(appSize);
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    public SslConnection(SSLEngine engine,EndPoint endp)
+    {
+        this(engine,endp,System.currentTimeMillis());
+    }
+
+    /* ------------------------------------------------------------ */
+    public SslConnection(SSLEngine engine,EndPoint endp, long timeStamp)
+    {
+        super(endp,timeStamp);
+        _engine=engine;
+        _session=_engine.getSession();
+        _aEndp=(AsyncEndPoint)endp;
+        _sslEndPoint = newSslEndPoint();
+    }
+
+    /* ------------------------------------------------------------ */
+    protected SslEndPoint newSslEndPoint()
+    {
+        return new SslEndPoint();
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * @return True if SSL re-negotiation is allowed (default false)
+     */
+    public boolean isAllowRenegotiate()
+    {
+        return _allowRenegotiate;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * Set if SSL re-negotiation is allowed. CVE-2009-3555 discovered
+     * a vulnerability in SSL/TLS with re-negotiation.  If your JVM
+     * does not have CVE-2009-3555 fixed, then re-negotiation should
+     * not be allowed.  CVE-2009-3555 was fixed in Sun java 1.6 with a ban
+     * of renegotiates in u19 and with RFC5746 in u22.
+     *
+     * @param allowRenegotiate
+     *            true if re-negotiation is allowed (default false)
+     */
+    public void setAllowRenegotiate(boolean allowRenegotiate)
+    {
+        _allowRenegotiate = allowRenegotiate;
+    }
+
+    /* ------------------------------------------------------------ */
+    private void allocateBuffers()
+    {
+        synchronized (this)
+        {
+            if (_allocations++==0)
+            {
+                if (_buffers==null)
+                {
+                    _buffers=__buffers.get();
+                    if (_buffers==null)
+                        _buffers=new SslBuffers(_session.getPacketBufferSize()*2,_session.getApplicationBufferSize()*2);
+                    _inbound=_buffers._in;
+                    _outbound=_buffers._out;
+                    _unwrapBuf=_buffers._unwrap;
+                    __buffers.set(null);
+                }
+            }
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    private void releaseBuffers()
+    {
+        synchronized (this)
+        {
+            if (--_allocations==0)
+            {
+                if (_buffers!=null &&
+                    _inbound.length()==0 &&
+                    _outbound.length()==0 &&
+                    _unwrapBuf.length()==0)
+                {
+                    _inbound=null;
+                    _outbound=null;
+                    _unwrapBuf=null;
+                    __buffers.set(_buffers);
+                    _buffers=null;
+                }
+            }
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    public Connection handle() throws IOException
+    {
+        try
+        {
+            allocateBuffers();
+
+            boolean progress=true;
+
+            while (progress)
+            {
+                progress=false;
+
+                // If we are handshook let the delegate connection
+                if (_engine.getHandshakeStatus()!=HandshakeStatus.NOT_HANDSHAKING)
+                    progress=process(null,null);
+
+                // handle the delegate connection
+                AsyncConnection next = (AsyncConnection)_connection.handle();
+                if (next!=_connection && next!=null)
+                {
+                    _connection=next;
+                    progress=true;
+                }
+
+                _logger.debug("{} handle {} progress={}", _session, this, progress);
+            }
+        }
+        finally
+        {
+            releaseBuffers();
+
+            if (!_ishut && _sslEndPoint.isInputShutdown() && _sslEndPoint.isOpen())
+            {
+                _ishut=true;
+                try
+                {
+                    _connection.onInputShutdown();
+                }
+                catch(Throwable x)
+                {
+                    _logger.warn("onInputShutdown failed", x);
+                    try{_sslEndPoint.close();}
+                    catch(IOException e2){
+                        _logger.ignore(e2);}
+                }
+            }
+        }
+
+        return this;
+    }
+
+    /* ------------------------------------------------------------ */
+    public boolean isIdle()
+    {
+        return false;
+    }
+
+    /* ------------------------------------------------------------ */
+    public boolean isSuspended()
+    {
+        return false;
+    }
+
+    /* ------------------------------------------------------------ */
+    public void onClose()
+    {
+        Connection connection = _sslEndPoint.getConnection();
+        if (connection != null && connection != this)
+            connection.onClose();
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public void onIdleExpired(long idleForMs)
+    {
+        try
+        {
+            _logger.debug("onIdleExpired {}ms on {}",idleForMs,this);
+            if (_endp.isOutputShutdown())
+                _sslEndPoint.close();
+            else
+                _sslEndPoint.shutdownOutput();
+        }
+        catch (IOException e)
+        {
+            _logger.warn(e);
+            super.onIdleExpired(idleForMs);
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    public void onInputShutdown() throws IOException
+    {
+
+    }
+
+    /* ------------------------------------------------------------ */
+    private synchronized boolean process(Buffer toFill, Buffer toFlush) throws IOException
+    {
+        boolean some_progress=false;
+        try
+        {
+            // We need buffers to progress
+            allocateBuffers();
+
+            // if we don't have a buffer to put received data into
+            if (toFill==null)
+            {
+                // use the unwrapbuffer to hold received data.
+                _unwrapBuf.compact();
+                toFill=_unwrapBuf;
+            }
+            // Else if the fill buffer is too small for the SSL session
+            else if (toFill.capacity()<_session.getApplicationBufferSize())
+            {
+                // fill to the temporary unwrapBuffer
+                boolean progress=process(null,toFlush);
+
+                // if we received any data,
+                if (_unwrapBuf!=null && _unwrapBuf.hasContent())
+                {
+                    // transfer from temp buffer to fill buffer
+                    _unwrapBuf.skip(toFill.put(_unwrapBuf));
+                    return true;
+                }
+                else
+                    // return progress from recursive call
+                    return progress;
+            }
+            // Else if there is some temporary data
+            else if (_unwrapBuf!=null && _unwrapBuf.hasContent())
+            {
+                // transfer from temp buffer to fill buffer
+                _unwrapBuf.skip(toFill.put(_unwrapBuf));
+                return true;
+            }
+
+            // If we are here, we have a buffer ready into which we can put some read data.
+
+            // If we have no data to flush, flush the empty buffer
+            if (toFlush==null)
+                toFlush=__ZERO_BUFFER;
+
+            // While we are making progress processing SSL engine
+            boolean progress=true;
+            while (progress)
+            {
+                progress=false;
+
+                // Do any real IO
+                int filled=0,flushed=0;
+                try
+                {
+                    // Read any available data
+                    if (_inbound.space()>0 && (filled=_endp.fill(_inbound))>0)
+                        progress = true;
+
+                    // flush any output data
+                    if (_outbound.hasContent() && (flushed=_endp.flush(_outbound))>0)
+                        progress = true;
+                }
+                catch (IOException e)
+                {
+                    _endp.close();
+                    throw e;
+                }
+                finally
+                {
+                    _logger.debug("{} {} {} filled={}/{} flushed={}/{}",_session,this,_engine.getHandshakeStatus(),filled,_inbound.length(),flushed,_outbound.length());
+                }
+
+                // handle the current hand share status
+                switch(_engine.getHandshakeStatus())
+                {
+                    case FINISHED:
+                        throw new IllegalStateException();
+
+                    case NOT_HANDSHAKING:
+                    {
+                        // Try unwrapping some application data
+                        if (toFill.space()>0 && _inbound.hasContent() && unwrap(toFill))
+                            progress=true;
+
+                        // Try wrapping some application data
+                        if (toFlush.hasContent() && _outbound.space()>0 && wrap(toFlush))
+                            progress=true;
+                    }
+                    break;
+
+                    case NEED_TASK:
+                    {
+                        // A task needs to be run, so run it!
+                        Runnable task;
+                        while ((task=_engine.getDelegatedTask())!=null)
+                        {
+                            progress=true;
+                            task.run();
+                        }
+
+                    }
+                    break;
+
+                    case NEED_WRAP:
+                    {
+                        // The SSL needs to send some handshake data to the other side
+                        if (_handshook && !_allowRenegotiate)
+                            _endp.close();
+                        else if (wrap(toFlush))
+                            progress=true;
+                    }
+                    break;
+
+                    case NEED_UNWRAP:
+                    {
+                        // The SSL needs to receive some handshake data from the other side
+                        if (_handshook && !_allowRenegotiate)
+                            _endp.close();
+                        else if (!_inbound.hasContent()&&filled==-1)
+                        {
+                            // No more input coming
+                            _endp.shutdownInput();
+                        }
+                        else if (unwrap(toFill))
+                            progress=true;
+                    }
+                    break;
+                }
+
+                // pass on ishut/oshut state
+                if (_endp.isOpen() && _endp.isInputShutdown() && !_inbound.hasContent())
+                    closeInbound();
+
+                if (_endp.isOpen() && _engine.isOutboundDone() && !_outbound.hasContent())
+                    _endp.shutdownOutput();
+
+                // remember if any progress has been made
+                some_progress|=progress;
+            }
+
+            // If we are reading into the temp buffer and it has some content, then we should be dispatched.
+            if (toFill==_unwrapBuf && _unwrapBuf.hasContent() && !_connection.isSuspended())
+                _aEndp.dispatch();
+        }
+        finally
+        {
+            releaseBuffers();
+            if (some_progress)
+                _progressed.set(true);
+        }
+        return some_progress;
+    }
+
+    private void closeInbound()
+    {
+        try
+        {
+            _engine.closeInbound();
+        }
+        catch (SSLException x)
+        {
+            _logger.debug(x);
+        }
+    }
+
+    private synchronized boolean wrap(final Buffer buffer) throws IOException
+    {
+        ByteBuffer bbuf=extractByteBuffer(buffer);
+        final SSLEngineResult result;
+
+        synchronized(bbuf)
+        {
+            _outbound.compact();
+            ByteBuffer out_buffer=_outbound.getByteBuffer();
+            synchronized(out_buffer)
+            {
+                try
+                {
+                    bbuf.position(buffer.getIndex());
+                    bbuf.limit(buffer.putIndex());
+                    out_buffer.position(_outbound.putIndex());
+                    out_buffer.limit(out_buffer.capacity());
+                    result=_engine.wrap(bbuf,out_buffer);
+                    if (_logger.isDebugEnabled())
+                        _logger.debug("{} wrap {} {} consumed={} produced={}",
+                            _session,
+                            result.getStatus(),
+                            result.getHandshakeStatus(),
+                            result.bytesConsumed(),
+                            result.bytesProduced());
+
+
+                    buffer.skip(result.bytesConsumed());
+                    _outbound.setPutIndex(_outbound.putIndex()+result.bytesProduced());
+                }
+                catch(SSLException e)
+                {
+                    _logger.debug(String.valueOf(_endp), e);
+                    _endp.close();
+                    throw e;
+                }
+                finally
+                {
+                    out_buffer.position(0);
+                    out_buffer.limit(out_buffer.capacity());
+                    bbuf.position(0);
+                    bbuf.limit(bbuf.capacity());
+                }
+            }
+        }
+
+        switch(result.getStatus())
+        {
+            case BUFFER_UNDERFLOW:
+                throw new IllegalStateException();
+
+            case BUFFER_OVERFLOW:
+                break;
+
+            case OK:
+                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
+                    _handshook=true;
+                break;
+
+            case CLOSED:
+                _logger.debug("wrap CLOSE {} {}",this,result);
+                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
+                    _endp.close();
+                break;
+
+            default:
+                _logger.debug("{} wrap default {}",_session,result);
+            throw new IOException(result.toString());
+        }
+
+        return result.bytesConsumed()>0 || result.bytesProduced()>0;
+    }
+
+    private synchronized boolean unwrap(final Buffer buffer) throws IOException
+    {
+        if (!_inbound.hasContent())
+            return false;
+
+        ByteBuffer bbuf=extractByteBuffer(buffer);
+        final SSLEngineResult result;
+
+        synchronized(bbuf)
+        {
+            ByteBuffer in_buffer=_inbound.getByteBuffer();
+            synchronized(in_buffer)
+            {
+                try
+                {
+                    bbuf.position(buffer.putIndex());
+                    bbuf.limit(buffer.capacity());
+                    in_buffer.position(_inbound.getIndex());
+                    in_buffer.limit(_inbound.putIndex());
+
+                    result=_engine.unwrap(in_buffer,bbuf);
+                    if (_logger.isDebugEnabled())
+                        _logger.debug("{} unwrap {} {} consumed={} produced={}",
+                            _session,
+                            result.getStatus(),
+                            result.getHandshakeStatus(),
+                            result.bytesConsumed(),
+                            result.bytesProduced());
+
+                    _inbound.skip(result.bytesConsumed());
+                    _inbound.compact();
+                    buffer.setPutIndex(buffer.putIndex()+result.bytesProduced());
+                }
+                catch(SSLException e)
+                {
+                    _logger.debug(String.valueOf(_endp), e);
+                    _endp.close();
+                    throw e;
+                }
+                finally
+                {
+                    in_buffer.position(0);
+                    in_buffer.limit(in_buffer.capacity());
+                    bbuf.position(0);
+                    bbuf.limit(bbuf.capacity());
+                }
+            }
+        }
+
+        switch(result.getStatus())
+        {
+            case BUFFER_UNDERFLOW:
+                if (_endp.isInputShutdown())
+                    _inbound.clear();
+                break;
+
+            case BUFFER_OVERFLOW:
+                if (_logger.isDebugEnabled()) _logger.debug("{} unwrap {} {}->{}",_session,result.getStatus(),_inbound.toDetailString(),buffer.toDetailString());
+                break;
+
+            case OK:
+                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
+                    _handshook=true;
+                break;
+
+            case CLOSED:
+                _logger.debug("unwrap CLOSE {} {}",this,result);
+                if (result.getHandshakeStatus()==HandshakeStatus.FINISHED)
+                    _endp.close();
+                break;
+
+            default:
+                _logger.debug("{} wrap default {}",_session,result);
+            throw new IOException(result.toString());
+        }
+
+        //if (LOG.isDebugEnabled() && result.bytesProduced()>0)
+        //    LOG.debug("{} unwrapped '{}'",_session,buffer);
+
+        return result.bytesConsumed()>0 || result.bytesProduced()>0;
+    }
+
+
+    /* ------------------------------------------------------------ */
+    private ByteBuffer extractByteBuffer(Buffer buffer)
+    {
+        if (buffer.buffer() instanceof NIOBuffer)
+            return ((NIOBuffer)buffer.buffer()).getByteBuffer();
+        return ByteBuffer.wrap(buffer.array());
+    }
+
+    /* ------------------------------------------------------------ */
+    public AsyncEndPoint getSslEndPoint()
+    {
+        return _sslEndPoint;
+    }
+
+    /* ------------------------------------------------------------ */
+    public String toString()
+    {
+        return String.format("%s %s", super.toString(), _sslEndPoint);
+    }
+
+    /* ------------------------------------------------------------ */
+    /* ------------------------------------------------------------ */
+    public class SslEndPoint implements AsyncEndPoint
+    {
+        public SSLEngine getSslEngine()
+        {
+            return _engine;
+        }
+
+        public AsyncEndPoint getEndpoint()
+        {
+            return _aEndp;
+        }
+
+        public void shutdownOutput() throws IOException
+        {
+            synchronized (SslConnection.this)
+            {
+                _logger.debug("{} ssl endp.oshut {}",_session,this);
+                _engine.closeOutbound();
+                _oshut=true;
+            }
+            flush();
+        }
+
+        public boolean isOutputShutdown()
+        {
+            synchronized (SslConnection.this)
+            {
+                return _oshut||!isOpen()||_engine.isOutboundDone();
+            }
+        }
+
+        public void shutdownInput() throws IOException
+        {
+            _logger.debug("{} ssl endp.ishut!",_session);
+            // We do not do a closeInput here, as SSL does not support half close.
+            // isInputShutdown works it out itself from buffer state and underlying endpoint state.
+        }
+
+        public boolean isInputShutdown()
+        {
+            synchronized (SslConnection.this)
+            {
+                return _endp.isInputShutdown() &&
+                !(_unwrapBuf!=null&&_unwrapBuf.hasContent()) &&
+                !(_inbound!=null&&_inbound.hasContent());
+            }
+        }
+
+        public void close() throws IOException
+        {
+            _logger.debug("{} ssl endp.close",_session);
+            _endp.close();
+        }
+
+        public int fill(Buffer buffer) throws IOException
+        {
+            int size=buffer.length();
+            process(buffer, null);
+
+            int filled=buffer.length()-size;
+
+            if (filled==0 && isInputShutdown())
+                return -1;
+            return filled;
+        }
+
+        public int flush(Buffer buffer) throws IOException
+        {
+            int size = buffer.length();
+            process(null, buffer);
+            return size-buffer.length();
+        }
+
+        public int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException
+        {
+            if (header!=null && header.hasContent())
+                return flush(header);
+            if (buffer!=null && buffer.hasContent())
+                return flush(buffer);
+            if (trailer!=null && trailer.hasContent())
+                return flush(trailer);
+            return 0;
+        }
+
+        public boolean blockReadable(long millisecs) throws IOException
+        {
+            long now = System.currentTimeMillis();
+            long end=millisecs>0?(now+millisecs):Long.MAX_VALUE;
+
+            while (now<end)
+            {
+                if (process(null,null))
+                    break;
+                _endp.blockReadable(end-now);
+                now = System.currentTimeMillis();
+            }
+
+            return now<end;
+        }
+
+        public boolean blockWritable(long millisecs) throws IOException
+        {
+            return _endp.blockWritable(millisecs);
+        }
+
+        public boolean isOpen()
+        {
+            return _endp.isOpen();
+        }
+
+        public Object getTransport()
+        {
+            return _endp;
+        }
+
+        public void flush() throws IOException
+        {
+            process(null, null);
+        }
+
+        public void dispatch()
+        {
+            _aEndp.dispatch();
+        }
+
+        public void asyncDispatch()
+        {
+            _aEndp.asyncDispatch();
+        }
+
+        public void scheduleWrite()
+        {
+            _aEndp.scheduleWrite();
+        }
+
+        public void onIdleExpired(long idleForMs)
+        {
+            _aEndp.onIdleExpired(idleForMs);
+        }
+
+        public void setCheckForIdle(boolean check)
+        {
+            _aEndp.setCheckForIdle(check);
+        }
+
+        public boolean isCheckForIdle()
+        {
+            return _aEndp.isCheckForIdle();
+        }
+
+        public void scheduleTimeout(Task task, long timeoutMs)
+        {
+            _aEndp.scheduleTimeout(task,timeoutMs);
+        }
+
+        public void cancelTimeout(Task task)
+        {
+            _aEndp.cancelTimeout(task);
+        }
+
+        public boolean isWritable()
+        {
+            return _aEndp.isWritable();
+        }
+
+        public boolean hasProgressed()
+        {
+            return _progressed.getAndSet(false);
+        }
+
+        public String getLocalAddr()
+        {
+            return _aEndp.getLocalAddr();
+        }
+
+        public String getLocalHost()
+        {
+            return _aEndp.getLocalHost();
+        }
+
+        public int getLocalPort()
+        {
+            return _aEndp.getLocalPort();
+        }
+
+        public String getRemoteAddr()
+        {
+            return _aEndp.getRemoteAddr();
+        }
+
+        public String getRemoteHost()
+        {
+            return _aEndp.getRemoteHost();
+        }
+
+        public int getRemotePort()
+        {
+            return _aEndp.getRemotePort();
+        }
+
+        public boolean isBlocking()
+        {
+            return false;
+        }
+
+        public int getMaxIdleTime()
+        {
+            return _aEndp.getMaxIdleTime();
+        }
+
+        public void setMaxIdleTime(int timeMs) throws IOException
+        {
+            _aEndp.setMaxIdleTime(timeMs);
+        }
+
+        public Connection getConnection()
+        {
+            return _connection;
+        }
+
+        public void setConnection(Connection connection)
+        {
+            _connection=(AsyncConnection)connection;
+        }
+
+        public String toString()
+        {
+            // Do NOT use synchronized (SslConnection.this)
+            // because it's very easy to deadlock when debugging is enabled.
+            // We do a best effort to print the right toString() and that's it.
+            Buffer inbound = _inbound;
+            Buffer outbound = _outbound;
+            Buffer unwrap = _unwrapBuf;
+            int i = inbound == null? -1 : inbound.length();
+            int o = outbound == null ? -1 : outbound.length();
+            int u = unwrap == null ? -1 : unwrap.length();
+            return String.format("SSL %s i/o/u=%d/%d/%d ishut=%b oshut=%b {%s}",
+                    _engine.getHandshakeStatus(),
+                    i, o, u,
+                    _ishut, _oshut,
+                    _connection);
+        }
+
+    }
+}