// ======================================================================== // Copyright (c) 2004-2011 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()) _engine.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.asyncDispatch(); } finally { releaseBuffers(); if (some_progress) _progressed.set(true); } return some_progress; } 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 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); } } }