From 41e46448d52a0d791c528bef9157ef08f95a1471 Mon Sep 17 00:00:00 2001 From: zzz <zzz@i2pmail.org> Date: Sat, 24 Apr 2021 15:56:51 -0400 Subject: [PATCH] Jetty: SslConnection.java unmodified from 9.3.29.v20201019 Patch to follow in next commit --- .../eclipse/jetty/io/ssl/SslConnection.java | 1326 +++++++++++++++++ 1 file changed, 1326 insertions(+) create mode 100644 apps/jetty/patches/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java diff --git a/apps/jetty/patches/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java b/apps/jetty/patches/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java new file mode 100644 index 0000000000..62d41ff7b4 --- /dev/null +++ b/apps/jetty/patches/jetty-io/src/main/java/org/eclipse/jetty/io/ssl/SslConnection.java @@ -0,0 +1,1326 @@ +// +// ======================================================================== +// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others. +// ------------------------------------------------------------------------ +// 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.ssl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.ToIntFunction; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.AbstractEndPoint; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.io.SelectChannelEndPoint; +import org.eclipse.jetty.io.WriteFlusher; +import org.eclipse.jetty.util.BufferUtil; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * A Connection that acts as an interceptor between an EndPoint providing SSL encrypted data + * and another consumer of an EndPoint (typically an {@link Connection} like HttpConnection) that + * wants unencrypted data. + * <p> + * The connector uses an {@link EndPoint} (typically {@link SelectChannelEndPoint}) as + * it's source/sink of encrypted data. It then provides an endpoint via {@link #getDecryptedEndPoint()} to + * expose a source/sink of unencrypted data to another connection (eg HttpConnection). + * <p> + * The design of this class is based on a clear separation between the passive methods, which do not block nor schedule any + * asynchronous callbacks, and active methods that do schedule asynchronous callbacks. + * <p> + * The passive methods are {@link DecryptedEndPoint#fill(ByteBuffer)} and {@link DecryptedEndPoint#flush(ByteBuffer...)}. They make best + * effort attempts to progress the connection using only calls to the encrypted {@link EndPoint#fill(ByteBuffer)} and {@link EndPoint#flush(ByteBuffer...)} + * methods. They will never block nor schedule any readInterest or write callbacks. If a fill/flush cannot progress either because + * of network congestion or waiting for an SSL handshake message, then the fill/flush will simply return with zero bytes filled/flushed. + * Specifically, if a flush cannot proceed because it needs to receive a handshake message, then the flush will attempt to fill bytes from the + * encrypted endpoint, but if insufficient bytes are read it will NOT call {@link EndPoint#fillInterested(Callback)}. + * <p> + * It is only the active methods : {@link DecryptedEndPoint#fillInterested(Callback)} and + * {@link DecryptedEndPoint#write(Callback, ByteBuffer...)} that may schedule callbacks by calling the encrypted + * {@link EndPoint#fillInterested(Callback)} and {@link EndPoint#write(Callback, ByteBuffer...)} + * methods. For normal data handling, the decrypted fillInterest method will result in an encrypted fillInterest and a decrypted + * write will result in an encrypted write. However, due to SSL handshaking requirements, it is also possible for a decrypted fill + * to call the encrypted write and for the decrypted flush to call the encrypted fillInterested methods. + * <p> + * MOST IMPORTANTLY, the encrypted callbacks from the active methods (#onFillable() and WriteFlusher#completeWrite()) do no filling or flushing + * themselves. Instead they simple make the callbacks to the decrypted callbacks, so that the passive encrypted fill/flush will + * be called again and make another best effort attempt to progress the connection. + * + */ +public class SslConnection extends AbstractConnection +{ + private static final Logger LOG = Log.getLogger(SslConnection.class); + + private final List<SslHandshakeListener> handshakeListeners = new ArrayList<>(); + private final ByteBufferPool _bufferPool; + private final SSLEngine _sslEngine; + private final DecryptedEndPoint _decryptedEndPoint; + private ByteBuffer _decryptedInput; + private ByteBuffer _encryptedInput; + private ByteBuffer _encryptedOutput; + private final boolean _encryptedDirectBuffers; + private final boolean _decryptedDirectBuffers; + private boolean _renegotiationAllowed; + private int _renegotiationLimit = -1; + private boolean _closedOutbound; + private boolean _allowMissingCloseMessage = true; + private final Runnable _runCompletWrite = new Runnable() + { + @Override + public void run() + { + _decryptedEndPoint.getWriteFlusher().completeWrite(); + } + }; + private final Runnable _runFillable = new Runnable() + { + @Override + public void run() + { + _decryptedEndPoint.getFillInterest().fillable(); + } + }; + private final Callback _nonBlockingReadCallback = new Callback.NonBlocking() + { + @Override + public void succeeded() + { + onFillable(); + } + + @Override + public void failed(final Throwable x) + { + onFillInterestedFailed(x); + } + + @Override + public String toString() + { + return String.format("SSLC.NBReadCB@%x{%s}", SslConnection.this.hashCode(),SslConnection.this); + } + }; + + public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine) + { + this(byteBufferPool, executor, endPoint, sslEngine, false, false); + } + + public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine, + boolean useDirectBuffersForEncryption, boolean useDirectBuffersForDecryption) + { + // This connection does not execute calls to onFillable(), so they will be called by the selector thread. + // onFillable() does not block and will only wakeup another thread to do the actual reading and handling. + super(endPoint, executor); + this._bufferPool = byteBufferPool; + this._sslEngine = sslEngine; + this._decryptedEndPoint = newDecryptedEndPoint(); + this._encryptedDirectBuffers = useDirectBuffersForEncryption; + this._decryptedDirectBuffers = useDirectBuffersForDecryption; + } + + public void addHandshakeListener(SslHandshakeListener listener) + { + handshakeListeners.add(listener); + } + + public boolean removeHandshakeListener(SslHandshakeListener listener) + { + return handshakeListeners.remove(listener); + } + + protected DecryptedEndPoint newDecryptedEndPoint() + { + return new DecryptedEndPoint(); + } + + public SSLEngine getSSLEngine() + { + return _sslEngine; + } + + public DecryptedEndPoint getDecryptedEndPoint() + { + return _decryptedEndPoint; + } + + public boolean isRenegotiationAllowed() + { + return _renegotiationAllowed; + } + + public void setRenegotiationAllowed(boolean renegotiationAllowed) + { + _renegotiationAllowed = renegotiationAllowed; + } + + /** + * @return The number of renegotions allowed for this connection. When the limit + * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. + */ + public int getRenegotiationLimit() + { + return _renegotiationLimit; + } + + /** + * @param renegotiationLimit The number of renegotions allowed for this connection. + * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied. + * Default -1. + */ + public void setRenegotiationLimit(int renegotiationLimit) + { + _renegotiationLimit = renegotiationLimit; + } + + public boolean isAllowMissingCloseMessage() + { + return _allowMissingCloseMessage; + } + + public void setAllowMissingCloseMessage(boolean allowMissingCloseMessage) + { + this._allowMissingCloseMessage = allowMissingCloseMessage; + } + + private int getApplicationBufferSize() + { + return getBufferSize(SSLSession::getApplicationBufferSize); + } + + private int getPacketBufferSize() + { + return getBufferSize(SSLSession::getPacketBufferSize); + } + + private int getBufferSize(ToIntFunction<SSLSession> bufferSizeFn) + { + SSLSession hsSession = _sslEngine.getHandshakeSession(); + SSLSession session = _sslEngine.getSession(); + int size = bufferSizeFn.applyAsInt(session); + if (hsSession == null || hsSession == session) + return size; + int hsSize = bufferSizeFn.applyAsInt(hsSession); + return Math.max(hsSize, size); + } + + private void acquireEncryptedInput() + { + if (_encryptedInput == null) + _encryptedInput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers); + } + + private void acquireEncryptedOutput() + { + if (_encryptedOutput == null) + _encryptedOutput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers); + } + + private void releaseEncryptedInputBuffer() + { + if (_encryptedInput != null && !_encryptedInput.hasRemaining()) + { + _bufferPool.release(_encryptedInput); + _encryptedInput = null; + } + } + + protected void releaseDecryptedInputBuffer() + { + if (_decryptedInput != null && !_decryptedInput.hasRemaining()) + { + _bufferPool.release(_decryptedInput); + _decryptedInput = null; + } + } + + @Override + public void onOpen() + { + super.onOpen(); + getDecryptedEndPoint().getConnection().onOpen(); + } + + @Override + public void onClose() + { + _decryptedEndPoint.getConnection().onClose(); + super.onClose(); + } + + @Override + public void close() + { + getDecryptedEndPoint().getConnection().close(); + } + + @Override + public boolean onIdleExpired() + { + return getDecryptedEndPoint().getConnection().onIdleExpired(); + } + + @Override + public void onFillable() + { + // onFillable means that there are encrypted bytes ready to be filled. + // however we do not fill them here on this callback, but instead wakeup + // the decrypted readInterest and/or writeFlusher so that they will attempt + // to do the fill and/or flush again and these calls will do the actually + // filling. + + if (LOG.isDebugEnabled()) + LOG.debug("onFillable enter {}", _decryptedEndPoint); + + // We have received a close handshake, close the end point to send FIN. + if (_decryptedEndPoint.isInputShutdown()) + _decryptedEndPoint.close(); + + // wake up whoever is doing the fill or the flush so they can + // do all the filling, unwrapping, wrapping and flushing + _decryptedEndPoint.getFillInterest().fillable(); + + // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read + boolean runComplete = false; + synchronized(_decryptedEndPoint) + { + if (_decryptedEndPoint._flushRequiresFillToProgress) + { + _decryptedEndPoint._flushRequiresFillToProgress = false; + runComplete = true; + } + } + if (runComplete) + _runCompletWrite.run(); + + if (LOG.isDebugEnabled()) + LOG.debug("onFillable exit {}", _decryptedEndPoint); + } + + @Override + public void onFillInterestedFailed(Throwable cause) + { + // this means that the fill interest in encrypted bytes has failed. + // However we do not handle that here on this callback, but instead wakeup + // the decrypted readInterest and/or writeFlusher so that they will attempt + // to do the fill and/or flush again and these calls will do the actually + // handle the cause. + _decryptedEndPoint.getFillInterest().onFail(cause); + + boolean failFlusher = false; + synchronized(_decryptedEndPoint) + { + if (_decryptedEndPoint._flushRequiresFillToProgress) + { + _decryptedEndPoint._flushRequiresFillToProgress = false; + failFlusher = true; + } + } + if (failFlusher) + _decryptedEndPoint.getWriteFlusher().onFail(cause); + } + + protected SSLEngineResult wrap(SSLEngine sslEngine, ByteBuffer[] input, ByteBuffer output) throws SSLException + { + return sslEngine.wrap(input, output); + } + + protected SSLEngineResult unwrap(SSLEngine sslEngine, ByteBuffer input, ByteBuffer output) throws SSLException + { + return sslEngine.unwrap(input, output); + } + + @Override + public String toString() + { + ByteBuffer b = _encryptedInput; + int ei=b==null?-1:b.remaining(); + b = _encryptedOutput; + int eo=b==null?-1:b.remaining(); + b = _decryptedInput; + int di=b==null?-1:b.remaining(); + + return String.format("SslConnection@%x{%s,eio=%d/%d,di=%d} -> %s", + hashCode(), + _sslEngine.getHandshakeStatus(), + ei,eo,di, + _decryptedEndPoint.getConnection()); + } + + public class DecryptedEndPoint extends AbstractEndPoint + { + private boolean _fillRequiresFlushToProgress; + private boolean _flushRequiresFillToProgress; + private boolean _cannotAcceptMoreAppDataToFlush; + private boolean _handshaken; + private boolean _underFlown; + + private final Callback _writeCallback = new Callback() + { + @Override + public void succeeded() + { + // This means that a write of encrypted data has completed. Writes are done + // only if there is a pending writeflusher or a read needed to write + // data. In either case the appropriate callback is passed on. + boolean fillable = false; + synchronized (DecryptedEndPoint.this) + { + if (LOG.isDebugEnabled()) + LOG.debug("write.complete {}", SslConnection.this.getEndPoint()); + + releaseEncryptedOutputBuffer(); + + _cannotAcceptMoreAppDataToFlush = false; + + if (_fillRequiresFlushToProgress) + { + _fillRequiresFlushToProgress = false; + fillable = true; + } + } + if (fillable) + getFillInterest().fillable(); + _runCompletWrite.run(); + } + + @Override + public void failed(final Throwable x) + { + // This means that a write of data has failed. Writes are done + // only if there is an active writeflusher or a read needed to write + // data. In either case the appropriate callback is passed on. + boolean fail_filler; + synchronized (DecryptedEndPoint.this) + { + if (LOG.isDebugEnabled()) + LOG.debug("write failed {}", SslConnection.this, x); + + BufferUtil.clear(_encryptedOutput); + releaseEncryptedOutputBuffer(); + + _cannotAcceptMoreAppDataToFlush = false; + fail_filler = _fillRequiresFlushToProgress; + if (_fillRequiresFlushToProgress) + _fillRequiresFlushToProgress = false; + } + + failedCallback(new Callback() + { + @Override + public void failed(Throwable x) + { + if (fail_filler) + getFillInterest().onFail(x); + getWriteFlusher().onFail(x); + } + }, x); + } + + @Override + public boolean isNonBlocking() + { + return getWriteFlusher().isCallbackNonBlocking(); + } + }; + + public DecryptedEndPoint() + { + // Disable idle timeout checking: no scheduler and -1 timeout for this instance. + super(null, getEndPoint().getLocalAddress(), getEndPoint().getRemoteAddress()); + super.setIdleTimeout(-1); + } + + @Override + public long getIdleTimeout() + { + return getEndPoint().getIdleTimeout(); + } + + @Override + public void setIdleTimeout(long idleTimeout) + { + getEndPoint().setIdleTimeout(idleTimeout); + } + + @Override + public boolean isOpen() + { + return getEndPoint().isOpen(); + } + + @Override + protected WriteFlusher getWriteFlusher() + { + return super.getWriteFlusher(); + } + + @Override + protected void onIncompleteFlush() + { + // This means that the decrypted endpoint write method was called and not + // all data could be wrapped. So either we need to write some encrypted data, + // OR if we are handshaking we need to read some encrypted data OR + // if neither then we should just try the flush again. + boolean try_again = false; + boolean write = false; + boolean need_fill_interest = false; + synchronized (DecryptedEndPoint.this) + { + if (LOG.isDebugEnabled()) + LOG.debug("onIncompleteFlush {}", SslConnection.this); + // If we have pending output data, + if (BufferUtil.hasContent(_encryptedOutput)) + { + // write it + _cannotAcceptMoreAppDataToFlush = true; + write = true; + } + // If we are handshaking and need to read, + else if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) + { + // check if we are actually read blocked in order to write + _flushRequiresFillToProgress = true; + need_fill_interest = !SslConnection.this.isFillInterested(); + } + else + { + // We can get here because the WriteFlusher might not see progress + // when it has just flushed the encrypted data, but not consumed anymore + // of the application buffers. This is mostly avoided by another iteration + // within DecryptedEndPoint flush(), but I cannot convince myself that + // this is never ever the case. + try_again = true; + } + } + + if (write) + getEndPoint().write(_writeCallback, _encryptedOutput); + else if (need_fill_interest) + ensureFillInterested(); + else if (try_again) + { + // If the output is closed, + if (isOutputShutdown()) + { + // don't bother writing, just notify of close + getWriteFlusher().onClose(); + } + // Else, + else + { + // try to flush what is pending + // execute to avoid recursion + getExecutor().execute(_runCompletWrite); + } + } + } + + @Override + protected void needsFillInterest() throws IOException + { + // This means that the decrypted data consumer has called the fillInterested + // method on the DecryptedEndPoint, so we have to work out if there is + // decrypted data to be filled or what callbacks to setup to be told when there + // might be more encrypted data available to attempt another call to fill + boolean fillable; + boolean write = false; + synchronized (DecryptedEndPoint.this) + { + // Do we already have some app data, then app can fill now so return true + fillable = (BufferUtil.hasContent(_decryptedInput)) + // or if we have encryptedInput and have not underflowed yet, the it is worth trying a fill + || BufferUtil.hasContent(_encryptedInput) && !_underFlown; + + // If we have no encrypted data to decrypt OR we have some, but it is not enough + if (!fillable) + { + // We are not ready to read data + + // Are we actually write blocked? + if (_fillRequiresFlushToProgress) + { + // we must be blocked trying to write before we can read + + // Do we have data to write + if (BufferUtil.hasContent(_encryptedOutput)) + { + // write it + _cannotAcceptMoreAppDataToFlush = true; + write = true; + } + else + { + // we have already written the net data + // pretend we are readable so the wrap is done by next readable callback + _fillRequiresFlushToProgress = false; + fillable=true; + } + } + } + } + if (write) + getEndPoint().write(_writeCallback, _encryptedOutput); + else if (fillable) + getExecutor().execute(_runFillable); + else + ensureFillInterested(); + } + + @Override + public void setConnection(Connection connection) + { + if (connection instanceof AbstractConnection) + { + // This is an optimization to avoid that upper layer connections use small + // buffers and we need to copy decrypted data rather than decrypting in place. + AbstractConnection c = (AbstractConnection)connection; + int appBufferSize = getApplicationBufferSize(); + if (c.getInputBufferSize() < appBufferSize) + c.setInputBufferSize(appBufferSize); + } + super.setConnection(connection); + } + + public SslConnection getSslConnection() + { + return SslConnection.this; + } + + @Override + public int fill(ByteBuffer buffer) throws IOException + { + try + { + synchronized (this) + { + Throwable failure = null; + try + { + // Do we already have some decrypted data? + if (BufferUtil.hasContent(_decryptedInput)) + return BufferUtil.append(buffer,_decryptedInput); + + // We will need a network buffer + if (_encryptedInput == null) + _encryptedInput = _bufferPool.acquire(_sslEngine.getSession().getPacketBufferSize(), _encryptedDirectBuffers); + else + BufferUtil.compact(_encryptedInput); + + + // loop filling and unwrapping until we have something + while (true) + { + // We also need an app buffer, but can use the passed buffer if it is big enough + ByteBuffer app_in; + int appBufferSize = getApplicationBufferSize(); + + if (BufferUtil.space(buffer) > appBufferSize) + app_in = buffer; + else if (_decryptedInput == null) + app_in = _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers); + else + app_in = _decryptedInput; + + acquireEncryptedInput(); + + // Let's try reading some encrypted data... even if we have some already. + int net_filled = getEndPoint().fill(_encryptedInput); + + if (net_filled > 0 && !_handshaken && isOutboundDone()) + throw new SSLHandshakeException("Closed during handshake"); + + decryption: while (true) + { + // Let's unwrap even if we have no net data because in that + // case we want to fall through to the handshake handling + int pos = BufferUtil.flipToFill(app_in); + SSLEngineResult unwrapResult; + try + { + unwrapResult = unwrap(_sslEngine, _encryptedInput, app_in); + } + finally + { + BufferUtil.flipToFlush(app_in, pos); + } + + if (LOG.isDebugEnabled()) + LOG.debug("unwrap net_filled={} {} encryptedBuffer={} unwrapBuffer={} appBuffer={}", + net_filled, + unwrapResult.toString().replace('\n',' '), + BufferUtil.toSummaryString(_encryptedInput), + BufferUtil.toDetailString(app_in), + BufferUtil.toDetailString(buffer)); + + HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); + HandshakeStatus unwrapHandshakeStatus = unwrapResult.getHandshakeStatus(); + Status unwrapResultStatus = unwrapResult.getStatus(); + + // Extra check on unwrapResultStatus == OK with zero bytes consumed + // or produced is due to an SSL client on Android (see bug #454773). + _underFlown = unwrapResultStatus == Status.BUFFER_UNDERFLOW || + unwrapResultStatus == Status.OK && unwrapResult.bytesConsumed() == 0 && unwrapResult.bytesProduced() == 0; + + if (_underFlown) + { + if (net_filled < 0 && _sslEngine.getUseClientMode()) + closeInbound(); + if (net_filled <= 0) + return net_filled; + } + + switch (unwrapResultStatus) + { + case CLOSED: + { + switch (handshakeStatus) + { + case NOT_HANDSHAKING: + // We were not handshaking, so just tell the app we are closed + return -1; + case NEED_TASK: + _sslEngine.getDelegatedTask().run(); + continue; + case NEED_WRAP: + // We need to send some handshake data (probably the close handshake). + // We return -1 so that the application can drive the close by flushing + // or shutting down the output. + return -1; + case NEED_UNWRAP: + // We expected to read more, but we got closed. + // Return -1 to indicate to the application to drive the close. + return -1; + default: + throw new IllegalStateException(); + } + } + case BUFFER_OVERFLOW: + // It's possible that SSLSession.applicationBufferSize has been expanded + // by the SSLEngine implementation. Unwrapping a large encrypted buffer + // causes BUFFER_OVERFLOW because the (old) applicationBufferSize is + // too small. Release the decrypted input buffer so it will be re-acquired + // with the larger capacity. + // See also system property "jsse.SSLEngine.acceptLargeFragments". + if (BufferUtil.isEmpty(_decryptedInput) && appBufferSize < getApplicationBufferSize()) + { + releaseDecryptedInputBuffer(); + break decryption; + } + throw new IllegalStateException("Unexpected unwrap result " + unwrapResultStatus); + + case BUFFER_UNDERFLOW: + case OK: + { + if (unwrapHandshakeStatus == HandshakeStatus.FINISHED) + handshakeFinished(); + + // Check whether re-negotiation is allowed + if (!allowRenegotiate(handshakeStatus)) + return -1; + + // If bytes were produced, don't bother with the handshake status; + // pass the decrypted data to the application, which will perform + // another call to fill() or flush(). + if (unwrapResult.bytesProduced() > 0) + { + if (app_in == buffer) + return unwrapResult.bytesProduced(); + return BufferUtil.append(buffer,_decryptedInput); + } + + switch (handshakeStatus) + { + case NOT_HANDSHAKING: + { + if (_underFlown) + break decryption; + continue; + } + case NEED_TASK: + { + _sslEngine.getDelegatedTask().run(); + continue; + } + case NEED_WRAP: + { + // If we are called from flush() + // return to let it do the wrapping. + if (_flushRequiresFillToProgress) + return 0; + + _fillRequiresFlushToProgress = true; + flush(BufferUtil.EMPTY_BUFFER); + if (BufferUtil.isEmpty(_encryptedOutput)) + { + // The flush wrote all the encrypted bytes so continue to fill. + _fillRequiresFlushToProgress = false; + if (_underFlown) + break decryption; + continue; + } + else + { + // The flush did not complete, return from fill() + // and let the write completion mechanism to kick in. + return 0; + } + } + case NEED_UNWRAP: + { + if (_underFlown) + break decryption; + continue; + } + default: + { + throw new IllegalStateException(); + } + } + } + default: + { + throw new IllegalStateException(); + } + } + } + } + } + catch (SSLHandshakeException x) + { + notifyHandshakeFailed(_sslEngine, x); + failure = x; + throw x; + } + catch (SSLException x) + { + if (!_handshaken) + { + x = (SSLException)new SSLHandshakeException(x.getMessage()).initCause(x); + notifyHandshakeFailed(_sslEngine, x); + } + failure = x; + throw x; + } + catch (Throwable x) + { + failure = x; + throw x; + } + finally + { + // If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read + if (_flushRequiresFillToProgress) + { + _flushRequiresFillToProgress = false; + getExecutor().execute(failure == null ? _runCompletWrite : new FailWrite(failure)); + } + + releaseEncryptedInputBuffer(); + releaseDecryptedInputBuffer(); + } + } + } + catch (Throwable x) + { + close(x); + if(x instanceof IOException) + throw (IOException) x; + throw new RuntimeIOException(x); + } + } + + private void handshakeFinished() + { + if (_handshaken) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiated {}", SslConnection.this); + if (_renegotiationLimit>0) + _renegotiationLimit--; + } + else + { + _handshaken = true; + if (LOG.isDebugEnabled()) + LOG.debug("{} handshake succeeded {}/{} {}", + _sslEngine.getUseClientMode() ? "client" : "resumed server", + _sslEngine.getSession().getProtocol(),_sslEngine.getSession().getCipherSuite(), + SslConnection.this); + notifyHandshakeSucceeded(_sslEngine); + } + } + + private boolean allowRenegotiate(HandshakeStatus handshakeStatus) + { + if (!_handshaken || handshakeStatus == HandshakeStatus.NOT_HANDSHAKING) + return true; + + if (!isRenegotiationAllowed()) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiation denied {}", SslConnection.this); + shutdownInput(); + return false; + } + + if (getRenegotiationLimit()==0) + { + if (LOG.isDebugEnabled()) + LOG.debug("Renegotiation limit exceeded {}", SslConnection.this); + shutdownInput(); + return false; + } + + return true; + } + + private void shutdownInput() + { + try + { + _sslEngine.closeInbound(); + } + catch (Throwable x) + { + LOG.ignore(x); + } + } + + private void closeInbound() throws SSLException + { + HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); + try + { + _sslEngine.closeInbound(); + } + catch (SSLException x) + { + if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && !isAllowMissingCloseMessage()) + throw x; + else + LOG.ignore(x); + } + catch (Throwable x) + { + LOG.ignore(x); + } + } + + @Override + public boolean flush(ByteBuffer... appOuts) throws IOException + { + // The contract for flush does not require that all appOuts bytes are written + // or even that any appOut bytes are written! If the connection is write block + // or busy handshaking, then zero bytes may be taken from appOuts and this method + // will return 0 (even if some handshake bytes were flushed and filled). + // it is the applications responsibility to call flush again - either in a busy loop + // or better yet by using EndPoint#write to do the flushing. + + if (LOG.isDebugEnabled()) + { + for (ByteBuffer b : appOuts) + LOG.debug("flush {} {}", BufferUtil.toHexSummary(b), SslConnection.this); + } + + try + { + synchronized (this) + { + try + { + if (_cannotAcceptMoreAppDataToFlush) + { + if (isOutboundDone()) + throw new EofException(new ClosedChannelException()); + return false; + } + + while (true) + { + int packetBufferSize = getPacketBufferSize(); + acquireEncryptedOutput(); + + // We call sslEngine.wrap to try to take bytes from appOuts + // buffers and encrypt them into the _encryptedOutput buffer. + BufferUtil.compact(_encryptedOutput); + int pos = BufferUtil.flipToFill(_encryptedOutput); + SSLEngineResult wrapResult; + try + { + wrapResult = wrap(_sslEngine, appOuts,_encryptedOutput); + } + finally + { + BufferUtil.flipToFlush(_encryptedOutput, pos); + } + if (LOG.isDebugEnabled()) + LOG.debug("wrap {} {}", wrapResult.toString().replace('\n',' '), SslConnection.this); + + Status wrapResultStatus = wrapResult.getStatus(); + + boolean allConsumed=true; + for (ByteBuffer b : appOuts) + if (BufferUtil.hasContent(b)) + allConsumed=false; + + // and deal with the results returned from the sslEngineWrap + switch (wrapResultStatus) + { + case CLOSED: + { + // The SSL engine has close, but there may be close handshake that needs to be written + if (BufferUtil.hasContent(_encryptedOutput)) + { + _cannotAcceptMoreAppDataToFlush = true; + getEndPoint().flush(_encryptedOutput); + getEndPoint().shutdownOutput(); + // If we failed to flush the close handshake then we will just pretend that + // the write has progressed normally and let a subsequent call to flush + // (or WriteFlusher#onIncompleteFlushed) to finish writing the close handshake. + // The caller will find out about the close on a subsequent flush or fill. + if (BufferUtil.hasContent(_encryptedOutput)) + return false; + } + // otherwise we have written, and the caller will close the underlying connection + else + { + getEndPoint().shutdownOutput(); + } + return allConsumed; + } + case BUFFER_UNDERFLOW: + { + throw new IllegalStateException(); + } + case BUFFER_OVERFLOW: + { + // It's possible that SSLSession.packetBufferSize has been expanded + // by the SSLEngine implementation. Wrapping a large application buffer + // causes BUFFER_OVERFLOW because the (old) packetBufferSize is + // too small. Release the encrypted output buffer so that it will + // be re-acquired with the larger capacity. + // See also system property "jsse.SSLEngine.acceptLargeFragments". + if (packetBufferSize < getPacketBufferSize()) + { + releaseEncryptedOutputBuffer(); + continue; + } + if (BufferUtil.isEmpty(_encryptedOutput)) + { + throw new IllegalStateException("Unexpected wrap result " + wrapResultStatus); + } + // fall-through default case to flush() + } + default: + { + if (LOG.isDebugEnabled()) + LOG.debug("wrap {} {} {}", wrapResultStatus, BufferUtil.toHexSummary(_encryptedOutput), SslConnection.this); + + if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) + handshakeFinished(); + + HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus(); + + // Check whether re-negotiation is allowed + if (!allowRenegotiate(handshakeStatus)) + { + getEndPoint().shutdownOutput(); + return allConsumed; + } + + // if we have net bytes, let's try to flush them + if (BufferUtil.hasContent(_encryptedOutput)) + if (!getEndPoint().flush(_encryptedOutput)) + getEndPoint().flush(_encryptedOutput); // one retry + + // But we also might have more to do for the handshaking state. + switch (handshakeStatus) + { + case NOT_HANDSHAKING: + // If we have not consumed all and had just finished handshaking, then we may + // have just flushed the last handshake in the encrypted buffers, so we should + // try again. + if (!allConsumed && wrapResult.getHandshakeStatus()==HandshakeStatus.FINISHED && BufferUtil.isEmpty(_encryptedOutput)) + continue; + + // Return true if we consumed all the bytes and encrypted are all flushed + return allConsumed && BufferUtil.isEmpty(_encryptedOutput); + + case NEED_TASK: + // run the task and continue + _sslEngine.getDelegatedTask().run(); + continue; + + case NEED_WRAP: + // Hey we just wrapped! Oh well who knows what the sslEngine is thinking, so continue and we will wrap again + continue; + + case NEED_UNWRAP: + // Ah we need to fill some data so we can write. + // So if we were not called from fill and the app is not reading anyway + if (!_fillRequiresFlushToProgress && !getFillInterest().isInterested()) + { + // Tell the onFillable method that there might be a write to complete + _flushRequiresFillToProgress = true; + fill(BufferUtil.EMPTY_BUFFER); + // Check if after the fill() we need to wrap again + if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) + continue; + } + return allConsumed && BufferUtil.isEmpty(_encryptedOutput); + + case FINISHED: + throw new IllegalStateException(); + } + } + } + } + } + catch (SSLHandshakeException x) + { + notifyHandshakeFailed(_sslEngine, x); + throw x; + } + finally + { + releaseEncryptedOutputBuffer(); + } + } + } + catch (Throwable x) + { + close(x); + throw x; + } + } + + private void releaseEncryptedOutputBuffer() + { + if (!Thread.holdsLock(DecryptedEndPoint.this)) + throw new IllegalStateException(); + if (_encryptedOutput != null && !_encryptedOutput.hasRemaining()) + { + _bufferPool.release(_encryptedOutput); + _encryptedOutput = null; + } + } + + @Override + public void shutdownOutput() + { + try + { + boolean flush = false; + boolean close = false; + synchronized (_decryptedEndPoint) + { + boolean ishut = isInputShutdown(); + boolean oshut = isOutputShutdown(); + if (LOG.isDebugEnabled()) + LOG.debug("shutdownOutput: oshut={}, ishut={} {}", oshut, ishut, SslConnection.this); + + if (oshut) + return; + + if (!_closedOutbound) + { + _closedOutbound=true; // Only attempt this once + closeOutbound(); + flush = true; + } + + // TODO review close logic here + if (ishut) + close = true; + } + + if (flush) + flush(BufferUtil.EMPTY_BUFFER); // Send the TLS close message. + if (close) + getEndPoint().close(); + else + ensureFillInterested(); + } + catch (Throwable x) + { + LOG.ignore(x); + getEndPoint().close(); + } + } + + private void closeOutbound() + { + try + { + _sslEngine.closeOutbound(); + } + catch (Throwable x) + { + LOG.ignore(x); + } + } + + private void ensureFillInterested() + { + if (getFillInterest().isCallbackNonBlocking()) + { + SslConnection.this.tryFillInterested(_nonBlockingReadCallback); + } + else + { + SslConnection.this.tryFillInterested(); + } + } + + @Override + public boolean isOutputShutdown() + { + return isOutboundDone() || getEndPoint().isOutputShutdown(); + } + + private boolean isOutboundDone() + { + try + { + return _sslEngine.isOutboundDone(); + } + catch (Throwable x) + { + LOG.ignore(x); + return true; + } + } + + @Override + public void close() + { + // First send the TLS Close Alert, then the FIN. + shutdownOutput(); + getEndPoint().close(); + super.close(); + } + + protected void close(Throwable failure) + { + // First send the TLS Close Alert, then the FIN. + shutdownOutput(); + getEndPoint().close(); + super.close(failure); + } + + @Override + public Object getTransport() + { + return getEndPoint(); + } + + @Override + public boolean isInputShutdown() + { + return getEndPoint().isInputShutdown() || isInboundDone(); + } + + private boolean isInboundDone() + { + try + { + return _sslEngine.isInboundDone(); + } + catch (Throwable x) + { + LOG.ignore(x); + return true; + } + } + + private void notifyHandshakeSucceeded(SSLEngine sslEngine) + { + SslHandshakeListener.Event event = null; + for (SslHandshakeListener listener : handshakeListeners) + { + if (event == null) + event = new SslHandshakeListener.Event(sslEngine); + try + { + listener.handshakeSucceeded(event); + } + catch (Throwable x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + } + + private void notifyHandshakeFailed(SSLEngine sslEngine, Throwable failure) + { + SslHandshakeListener.Event event = null; + for (SslHandshakeListener listener : handshakeListeners) + { + if (event == null) + event = new SslHandshakeListener.Event(sslEngine); + try + { + listener.handshakeFailed(event, failure); + } + catch (Throwable x) + { + LOG.info("Exception while notifying listener " + listener, x); + } + } + } + + @Override + public String toString() + { + return super.toString()+"->"+getEndPoint().toString(); + } + + private class FailWrite implements Runnable + { + private final Throwable failure; + + private FailWrite(Throwable failure) + { + this.failure = failure; + } + + @Override + public void run() + { + getWriteFlusher().onFail(failure); + } + } + } +} -- GitLab