//
// ========================================================================
// Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under
// the terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0
//
// This Source Code may also be made available under the following
// Secondary Licenses when the conditions for such availability set
// forth in the Eclipse Public License, v. 2.0 are satisfied:
// the Apache License v2.0 which is available at
// https://www.apache.org/licenses/LICENSE-2.0
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.io.ssl;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
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.WriteFlusher;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Invocable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
A Connection that acts as an interceptor between an EndPoint providing SSL encrypted data and another consumer of an EndPoint (typically an Connection
like HttpConnection) that wants unencrypted data. The connector uses an EndPoint
(typically SocketChannelEndPoint) as it's source/sink of encrypted data. It then provides an endpoint via getDecryptedEndPoint()
to expose a source/sink of unencrypted data to another connection (eg HttpConnection).
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.
The passive methods are DecryptedEndPoint.fill(ByteBuffer)
and DecryptedEndPoint.flush(ByteBuffer...)
. They make best effort attempts to progress the connection using only calls to the encrypted EndPoint.fill(ByteBuffer)
and 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 EndPoint.fillInterested(Callback)
.
It is only the active methods : AbstractEndPoint.fillInterested(Callback)
and AbstractEndPoint.write(Callback, ByteBuffer...)
that may schedule callbacks by calling the encrypted EndPoint.fillInterested(Callback)
and 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.
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.
/**
* 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 SocketChannelEndPoint) 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 implements Connection.UpgradeTo
{
private static final Logger LOG = LoggerFactory.getLogger(SslConnection.class);
private static final String TLS_1_3 = "TLSv1.3";
private enum HandshakeState
{
INITIAL,
HANDSHAKE,
SUCCEEDED,
FAILED
}
private enum FillState
{
IDLE, // Not Filling any data
INTERESTED, // We have a pending read interest
WAIT_FOR_FLUSH // Waiting for a flush to happen
}
private enum FlushState
{
IDLE, // Not flushing any data
WRITING, // We have a pending write of encrypted data
WAIT_FOR_FILL // Waiting for a fill to happen
}
private final AutoLock _lock = new AutoLock();
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 _requireCloseMessage;
private FlushState _flushState = FlushState.IDLE;
private FillState _fillState = FillState.IDLE;
private AtomicReference<HandshakeState> _handshake = new AtomicReference<>(HandshakeState.INITIAL);
private boolean _underflown;
private abstract class RunnableTask implements Runnable, Invocable
{
private final String _operation;
protected RunnableTask(String op)
{
_operation = op;
}
@Override
public String toString()
{
return String.format("SSL:%s:%s:%s", SslConnection.this, _operation, getInvocationType());
}
}
private final Runnable _runFillable = new RunnableTask("runFillable")
{
@Override
public void run()
{
_decryptedEndPoint.getFillInterest().fillable();
}
@Override
public InvocationType getInvocationType()
{
return _decryptedEndPoint.getFillInterest().getCallbackInvocationType();
}
};
private final Callback _sslReadCallback = new Callback()
{
@Override
public void succeeded()
{
onFillable();
}
@Override
public void failed(final Throwable x)
{
onFillInterestedFailed(x);
}
@Override
public InvocationType getInvocationType()
{
return getDecryptedEndPoint().getFillInterest().getCallbackInvocationType();
}
@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;
}
Returns: The number of renegotiations 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.
/**
* @return The number of renegotiations 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;
}
Params: - renegotiationLimit – The number of renegotiations 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.
/**
* @param renegotiationLimit The number of renegotiations 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;
}
Returns: whether peers must send the TLS close_notify
message
/**
* @return whether peers must send the TLS {@code close_notify} message
*/
public boolean isRequireCloseMessage()
{
return _requireCloseMessage;
}
Sets whether it is required that a peer send the TLS close_notify
message to indicate the will to close the connection, otherwise it may be interpreted as a truncation attack.
This option is only useful on clients, since typically servers cannot accept
connection-delimited content that may be truncated.
Params: - requireCloseMessage – whether peers must send the TLS
close_notify
message
/**
* <p>Sets whether it is required that a peer send the TLS {@code close_notify} message
* to indicate the will to close the connection, otherwise it may be interpreted as a
* truncation attack.</p>
* <p>This option is only useful on clients, since typically servers cannot accept
* connection-delimited content that may be truncated.</p>
*
* @param requireCloseMessage whether peers must send the TLS {@code close_notify} message
*/
public void setRequireCloseMessage(boolean requireCloseMessage)
{
_requireCloseMessage = requireCloseMessage;
}
private boolean isHandshakeInitial()
{
return _handshake.get() == HandshakeState.INITIAL;
}
private boolean isHandshakeSucceeded()
{
return _handshake.get() == HandshakeState.SUCCEEDED;
}
private boolean isHandshakeComplete()
{
HandshakeState state = _handshake.get();
return state == HandshakeState.SUCCEEDED || state == HandshakeState.FAILED;
}
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);
}
@Override
public void onUpgradeTo(ByteBuffer buffer)
{
acquireEncryptedInput();
BufferUtil.append(_encryptedInput, buffer);
}
@Override
public void onOpen()
{
super.onOpen();
getDecryptedEndPoint().getConnection().onOpen();
}
@Override
public void onClose(Throwable cause)
{
_decryptedEndPoint.getConnection().onClose(cause);
super.onClose(cause);
}
@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(">c.onFillable {}", SslConnection.this);
// We have received a close handshake, close the end point to send FIN.
if (_decryptedEndPoint.isInputShutdown())
_decryptedEndPoint.close();
_decryptedEndPoint.onFillable();
if (LOG.isDebugEnabled())
LOG.debug("<c.onFillable {}", SslConnection.this);
}
@Override
public void onFillInterestedFailed(Throwable cause)
{
_decryptedEndPoint.onFillableFail(cause == null ? new IOException() : 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 toConnectionString()
{
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();
Connection connection = _decryptedEndPoint.getConnection();
return String.format("%s@%x{%s,eio=%d/%d,di=%d,fill=%s,flush=%s}~>%s=>%s",
getClass().getSimpleName(),
hashCode(),
_sslEngine.getHandshakeStatus(),
ei, eo, di,
_fillState, _flushState,
_decryptedEndPoint.toEndPointString(),
connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection);
}
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;
}
}
private void releaseEncryptedOutputBuffer()
{
if (!_lock.isHeldByCurrentThread())
throw new IllegalStateException();
if (_encryptedOutput != null && !_encryptedOutput.hasRemaining())
{
_bufferPool.release(_encryptedOutput);
_encryptedOutput = null;
}
}
protected int networkFill(ByteBuffer input) throws IOException
{
return getEndPoint().fill(input);
}
protected boolean networkFlush(ByteBuffer output) throws IOException
{
return getEndPoint().flush(output);
}
public class DecryptedEndPoint extends AbstractEndPoint implements EndPoint.Wrapper
{
private final Callback _incompleteWriteCallback = new IncompleteWriteCallback();
private Throwable _failure;
public DecryptedEndPoint()
{
// Disable idle timeout checking: no scheduler and -1 timeout for this instance.
super(null);
super.setIdleTimeout(-1);
}
@Override
public EndPoint unwrap()
{
return getEndPoint();
}
@Override
public long getIdleTimeout()
{
return getEndPoint().getIdleTimeout();
}
@Override
public void setIdleTimeout(long idleTimeout)
{
getEndPoint().setIdleTimeout(idleTimeout);
}
@Override
public boolean isOpen()
{
return getEndPoint().isOpen();
}
@Override
public InetSocketAddress getLocalAddress()
{
return getEndPoint().getLocalAddress();
}
@Override
public InetSocketAddress getRemoteAddress()
{
return getEndPoint().getRemoteAddress();
}
@Override
public WriteFlusher getWriteFlusher()
{
return super.getWriteFlusher();
}
protected void onFillable()
{
try
{
// If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read
boolean waitingForFill;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onFillable {}", SslConnection.this);
_fillState = FillState.IDLE;
waitingForFill = _flushState == FlushState.WAIT_FOR_FILL;
}
getFillInterest().fillable();
if (waitingForFill)
{
try (AutoLock l = _lock.lock())
{
waitingForFill = _flushState == FlushState.WAIT_FOR_FILL;
}
if (waitingForFill)
fill(BufferUtil.EMPTY_BUFFER);
}
}
catch (Throwable e)
{
close(e);
}
}
protected void onFillableFail(Throwable failure)
{
// If we are handshaking, then wake up any waiting write as well as it may have been blocked on the read
boolean fail = false;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("onFillableFail {}", SslConnection.this, failure);
_fillState = FillState.IDLE;
if (_flushState == FlushState.WAIT_FOR_FILL)
{
_flushState = FlushState.IDLE;
fail = true;
}
}
// wake up whoever is doing the fill
getFillInterest().onFail(failure);
// Try to complete the write
if (fail)
{
if (!getWriteFlusher().onFail(failure))
close(failure);
}
}
@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
{
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug(">fill {}", SslConnection.this);
int filled = -2;
try
{
if (_fillState != FillState.IDLE)
return filled = 0;
// Do we already have some decrypted data?
if (BufferUtil.hasContent(_decryptedInput))
return filled = BufferUtil.append(buffer, _decryptedInput);
// loop filling and unwrapping until we have something
while (true)
{
HandshakeStatus status = _sslEngine.getHandshakeStatus();
if (LOG.isDebugEnabled())
LOG.debug("fill {}", status);
switch (status)
{
case NEED_UNWRAP:
case NOT_HANDSHAKING:
break;
case NEED_TASK:
_sslEngine.getDelegatedTask().run();
continue;
case NEED_WRAP:
if (_flushState == FlushState.IDLE && flush(BufferUtil.EMPTY_BUFFER))
{
Throwable failure = _failure;
if (failure != null)
rethrow(failure);
if (_sslEngine.isInboundDone())
return filled = -1;
continue;
}
// Handle in needsFillInterest().
return filled = 0;
default:
throw new IllegalStateException("Unexpected HandshakeStatus " + status);
}
acquireEncryptedInput();
// can we use the passed buffer if it is big enough
ByteBuffer appIn;
int appBufferSize = getApplicationBufferSize();
if (_decryptedInput == null)
{
if (BufferUtil.space(buffer) > appBufferSize)
appIn = buffer;
else
appIn = _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers);
}
else
{
appIn = _decryptedInput;
BufferUtil.compact(_encryptedInput);
}
// Let's try reading some encrypted data... even if we have some already.
int netFilled = networkFill(_encryptedInput);
if (LOG.isDebugEnabled())
LOG.debug("net filled={}", netFilled);
// Workaround for Java 11 behavior.
if (netFilled < 0 && isHandshakeInitial() && BufferUtil.isEmpty(_encryptedInput))
closeInbound();
if (netFilled > 0 && !isHandshakeComplete() && isOutboundDone())
throw new SSLHandshakeException("Closed during handshake");
if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE))
{
if (LOG.isDebugEnabled())
LOG.debug("fill starting handshake {}", SslConnection.this);
}
// 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(appIn);
SSLEngineResult unwrapResult;
try
{
_underflown = false;
unwrapResult = SslConnection.this.unwrap(_sslEngine, _encryptedInput, appIn);
}
finally
{
BufferUtil.flipToFlush(appIn, pos);
}
if (LOG.isDebugEnabled())
LOG.debug("unwrap net_filled={} {} encryptedBuffer={} unwrapBuffer={} appBuffer={}",
netFilled,
StringUtil.replace(unwrapResult.toString(), '\n', ' '),
BufferUtil.toSummaryString(_encryptedInput),
BufferUtil.toDetailString(appIn),
BufferUtil.toDetailString(buffer));
SSLEngineResult.Status unwrap = unwrapResult.getStatus();
// Extra check on unwrapResultStatus == OK with zero bytes consumed
// or produced is due to an SSL client on Android (see bug #454773).
if (unwrap == Status.OK && unwrapResult.bytesConsumed() == 0 && unwrapResult.bytesProduced() == 0)
unwrap = Status.BUFFER_UNDERFLOW;
switch (unwrap)
{
case CLOSED:
Throwable failure = _failure;
if (failure != null)
rethrow(failure);
return filled = -1;
case BUFFER_UNDERFLOW:
if (netFilled > 0)
continue; // try filling some more
_underflown = true;
if (netFilled < 0 && _sslEngine.getUseClientMode())
{
Throwable closeFailure = closeInbound();
if (_flushState == FlushState.WAIT_FOR_FILL)
{
Throwable handshakeFailure = new SSLHandshakeException("Abruptly closed by peer");
if (closeFailure != null)
handshakeFailure.initCause(closeFailure);
throw handshakeFailure;
}
return filled = -1;
}
return filled = netFilled;
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();
continue;
}
throw new IllegalStateException("Unexpected unwrap result " + unwrap);
case OK:
if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
handshakeSucceeded();
if (isRenegotiating() && !allowRenegotiate())
return filled = -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 (appIn == buffer)
return filled = unwrapResult.bytesProduced();
return filled = BufferUtil.append(buffer, _decryptedInput);
}
break;
default:
throw new IllegalStateException("Unexpected unwrap result " + unwrap);
}
}
}
catch (Throwable x)
{
Throwable f = handleException(x, "fill");
Throwable failure = handshakeFailed(f);
if (_flushState == FlushState.WAIT_FOR_FILL)
{
_flushState = FlushState.IDLE;
getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().onFail(failure));
}
throw failure;
}
finally
{
releaseEncryptedInputBuffer();
releaseDecryptedInputBuffer();
if (_flushState == FlushState.WAIT_FOR_FILL)
{
_flushState = FlushState.IDLE;
getExecutor().execute(() -> _decryptedEndPoint.getWriteFlusher().completeWrite());
}
if (LOG.isDebugEnabled())
LOG.debug("<fill f={} uf={} {}", filled, _underflown, SslConnection.this);
}
}
}
catch (Throwable x)
{
close(x);
rethrow(x);
// Never reached.
throw new AssertionError();
}
}
@Override
protected void needsFillInterest()
{
try
{
boolean fillable;
ByteBuffer write = null;
boolean interest = false;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug(">needFillInterest s={}/{} uf={} ei={} di={} {}",
_flushState,
_fillState,
_underflown,
BufferUtil.toDetailString(_encryptedInput),
BufferUtil.toDetailString(_decryptedInput),
SslConnection.this);
if (_fillState != FillState.IDLE)
return;
// Fillable if we have decrypted input OR enough encrypted input.
fillable = BufferUtil.hasContent(_decryptedInput) || (BufferUtil.hasContent(_encryptedInput) && !_underflown);
HandshakeStatus status = _sslEngine.getHandshakeStatus();
switch (status)
{
case NEED_TASK:
// Pretend we are fillable
fillable = true;
break;
case NEED_UNWRAP:
case NOT_HANDSHAKING:
if (!fillable)
{
interest = true;
_fillState = FillState.INTERESTED;
if (_flushState == FlushState.IDLE && BufferUtil.hasContent(_encryptedOutput))
{
_flushState = FlushState.WRITING;
write = _encryptedOutput;
}
}
break;
case NEED_WRAP:
if (!fillable)
{
_fillState = FillState.WAIT_FOR_FLUSH;
if (_flushState == FlushState.IDLE)
{
_flushState = FlushState.WRITING;
write = BufferUtil.hasContent(_encryptedOutput) ? _encryptedOutput : BufferUtil.EMPTY_BUFFER;
}
}
break;
default:
throw new IllegalStateException("Unexpected HandshakeStatus " + status);
}
if (LOG.isDebugEnabled())
LOG.debug("<needFillInterest s={}/{} f={} i={} w={}", _flushState, _fillState, fillable, interest, BufferUtil.toDetailString(write));
}
if (write != null)
getEndPoint().write(_incompleteWriteCallback, write);
else if (fillable)
getExecutor().execute(_runFillable);
else if (interest)
ensureFillInterested();
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug(SslConnection.this.toString(), x);
close(x);
throw x;
}
}
private void handshakeSucceeded() throws SSLException
{
if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.SUCCEEDED))
{
if (LOG.isDebugEnabled())
LOG.debug("handshake succeeded {} {} {}/{}", SslConnection.this,
_sslEngine.getUseClientMode() ? "client" : "resumed server",
_sslEngine.getSession().getProtocol(), _sslEngine.getSession().getCipherSuite());
notifyHandshakeSucceeded(_sslEngine);
}
else if (isHandshakeSucceeded())
{
if (_renegotiationLimit > 0)
_renegotiationLimit--;
}
}
private Throwable handshakeFailed(Throwable failure)
{
if (_handshake.compareAndSet(HandshakeState.HANDSHAKE, HandshakeState.FAILED))
{
if (LOG.isDebugEnabled())
LOG.debug("handshake failed {} {}", SslConnection.this, failure);
if (!(failure instanceof SSLHandshakeException))
failure = new SSLHandshakeException(failure.getMessage()).initCause(failure);
notifyHandshakeFailed(_sslEngine, failure);
}
return failure;
}
private void terminateInput()
{
try
{
_sslEngine.closeInbound();
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
}
}
private Throwable closeInbound() throws SSLException
{
HandshakeStatus handshakeStatus = _sslEngine.getHandshakeStatus();
try
{
_sslEngine.closeInbound();
return null;
}
catch (SSLException x)
{
if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && isRequireCloseMessage())
throw x;
LOG.trace("IGNORED", x);
return x;
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
return x;
}
}
@Override
public boolean flush(ByteBuffer... appOuts) throws IOException
{
try
{
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
{
LOG.debug(">flush {}", SslConnection.this);
int i = 0;
for (ByteBuffer b : appOuts)
{
LOG.debug("flush b[{}]={}", i++, BufferUtil.toDetailString(b));
}
}
// finish of any previous flushes
if (BufferUtil.hasContent(_encryptedOutput) && !networkFlush(_encryptedOutput))
return false;
boolean isEmpty = BufferUtil.isEmpty(appOuts);
Boolean result = null;
try
{
if (_flushState != FlushState.IDLE)
return result = false;
// Keep going while we can make progress or until we are done
while (true)
{
HandshakeStatus status = _sslEngine.getHandshakeStatus();
if (LOG.isDebugEnabled())
LOG.debug("flush {}", status);
switch (status)
{
case NEED_WRAP:
case NOT_HANDSHAKING:
break;
case NEED_TASK:
_sslEngine.getDelegatedTask().run();
continue;
case NEED_UNWRAP:
// Workaround for Java 11 behavior.
if (isHandshakeInitial() && isOutboundDone())
break;
if (_fillState == FillState.IDLE)
{
int filled = fill(BufferUtil.EMPTY_BUFFER);
if (_sslEngine.getHandshakeStatus() != status)
continue;
if (filled < 0)
throw new IOException("Broken pipe");
}
return result = isEmpty;
default:
throw new IllegalStateException("Unexpected HandshakeStatus " + status);
}
int packetBufferSize = getPacketBufferSize();
acquireEncryptedOutput();
if (_handshake.compareAndSet(HandshakeState.INITIAL, HandshakeState.HANDSHAKE))
{
if (LOG.isDebugEnabled())
LOG.debug("flush starting handshake {}", SslConnection.this);
}
// 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 {} {} ioDone={}/{}",
StringUtil.replace(wrapResult.toString(), '\n', ' '),
BufferUtil.toSummaryString(_encryptedOutput),
_sslEngine.isInboundDone(),
_sslEngine.isOutboundDone());
// Was all the data consumed?
isEmpty = BufferUtil.isEmpty(appOuts);
// if we have net bytes, let's try to flush them
boolean flushed = true;
if (BufferUtil.hasContent(_encryptedOutput))
flushed = networkFlush(_encryptedOutput);
if (LOG.isDebugEnabled())
LOG.debug("net flushed={}, ac={}", flushed, isEmpty);
// Now deal with the results returned from the wrap
Status wrap = wrapResult.getStatus();
switch (wrap)
{
case CLOSED:
{
// TODO: do we need to remember the CLOSED state or SSLEngine
// TODO: will produce CLOSED again if wrap() is called again?
if (!flushed)
return result = false;
getEndPoint().shutdownOutput();
if (isEmpty)
return result = true;
throw new IOException("Broken pipe");
}
case BUFFER_OVERFLOW:
if (!flushed)
return result = false;
// 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;
}
throw new IllegalStateException("Unexpected wrap result " + wrap);
case OK:
if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
handshakeSucceeded();
if (isRenegotiating() && !allowRenegotiate())
{
getEndPoint().shutdownOutput();
if (isEmpty && BufferUtil.isEmpty(_encryptedOutput))
return result = true;
throw new IOException("Broken pipe");
}
if (!flushed)
return result = false;
if (isEmpty)
{
if (wrapResult.getHandshakeStatus() != HandshakeStatus.NEED_WRAP ||
wrapResult.bytesProduced() == 0)
return result = true;
}
break;
default:
throw new IllegalStateException("Unexpected wrap result " + wrap);
}
if (getEndPoint().isOutputShutdown())
return false;
}
}
catch (Throwable x)
{
Throwable failure = handleException(x, "flush");
throw handshakeFailed(failure);
}
finally
{
releaseEncryptedOutputBuffer();
if (LOG.isDebugEnabled())
LOG.debug("<flush {} {}", result, SslConnection.this);
}
}
}
catch (Throwable x)
{
close(x);
rethrow(x);
// Never reached.
throw new AssertionError();
}
}
@Override
protected void onIncompleteFlush()
{
try
{
boolean fillInterest = false;
ByteBuffer write = null;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug(">onIncompleteFlush {} {}", SslConnection.this, BufferUtil.toDetailString(_encryptedOutput));
if (_flushState != FlushState.IDLE)
return;
while (true)
{
HandshakeStatus status = _sslEngine.getHandshakeStatus();
switch (status)
{
case NEED_TASK:
case NEED_WRAP:
case NOT_HANDSHAKING:
// write what we have or an empty buffer to reschedule a call to flush
write = BufferUtil.hasContent(_encryptedOutput) ? _encryptedOutput : BufferUtil.EMPTY_BUFFER;
_flushState = FlushState.WRITING;
break;
case NEED_UNWRAP:
// If we have something to write, then write it and ignore the needed unwrap for now.
if (BufferUtil.hasContent(_encryptedOutput))
{
write = _encryptedOutput;
_flushState = FlushState.WRITING;
break;
}
if (_fillState != FillState.IDLE)
{
// Wait for a fill that is happening anyway
_flushState = FlushState.WAIT_FOR_FILL;
break;
}
// Try filling ourselves
try
{
int filled = fill(BufferUtil.EMPTY_BUFFER);
// If this changed the status, let's try again
if (_sslEngine.getHandshakeStatus() != status)
continue;
if (filled < 0)
throw new IOException("Broken pipe");
}
catch (IOException e)
{
LOG.debug("Incomplete flush?", e);
close(e);
write = BufferUtil.EMPTY_BUFFER;
_flushState = FlushState.WRITING;
break;
}
// Make sure we are fill interested.
fillInterest = true;
_fillState = FillState.INTERESTED;
_flushState = FlushState.WAIT_FOR_FILL;
break;
default:
throw new IllegalStateException("Unexpected HandshakeStatus " + status);
}
break;
}
if (LOG.isDebugEnabled())
LOG.debug("<onIncompleteFlush s={}/{} fi={} w={}", _flushState, _fillState, fillInterest, BufferUtil.toDetailString(write));
}
if (write != null)
getEndPoint().write(_incompleteWriteCallback, write);
else if (fillInterest)
ensureFillInterested();
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug(SslConnection.this.toString(), x);
close(x);
throw x;
}
}
@Override
public void doShutdownOutput()
{
EndPoint endPoint = getEndPoint();
try
{
boolean close;
boolean flush = false;
try (AutoLock l = _lock.lock())
{
boolean ishut = endPoint.isInputShutdown();
boolean oshut = endPoint.isOutputShutdown();
if (LOG.isDebugEnabled())
LOG.debug("shutdownOutput: {} oshut={}, ishut={}", SslConnection.this, oshut, ishut);
closeOutbound();
if (!_closedOutbound)
{
_closedOutbound = true;
// Flush only once.
flush = !oshut;
}
close = ishut;
}
if (flush)
{
if (!flush(BufferUtil.EMPTY_BUFFER) && !close)
{
// If we still can't flush, but we are not closing the endpoint,
// let's just flush the encrypted output in the background.
ByteBuffer write = null;
try (AutoLock l = _lock.lock())
{
if (BufferUtil.hasContent(_encryptedOutput))
{
write = _encryptedOutput;
_flushState = FlushState.WRITING;
}
}
if (write != null)
{
endPoint.write(Callback.from(() ->
{
try (AutoLock l = _lock.lock())
{
_flushState = FlushState.IDLE;
releaseEncryptedOutputBuffer();
}
}, t -> endPoint.close()), write);
}
}
}
if (close)
endPoint.close();
else
ensureFillInterested();
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
endPoint.close();
}
}
private void closeOutbound()
{
try
{
_sslEngine.closeOutbound();
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Unable to close outbound", x);
}
}
private void ensureFillInterested()
{
if (LOG.isDebugEnabled())
LOG.debug("ensureFillInterested {}", SslConnection.this);
SslConnection.this.tryFillInterested(_sslReadCallback);
}
@Override
public boolean isOutputShutdown()
{
return isOutboundDone() || getEndPoint().isOutputShutdown();
}
private boolean isOutboundDone()
{
try
{
return _sslEngine.isOutboundDone();
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
return true;
}
}
@Override
public void doClose()
{
// First send the TLS Close Alert, then the FIN.
doShutdownOutput();
getEndPoint().close();
super.doClose();
}
@Override
public Object getTransport()
{
return getEndPoint();
}
@Override
public boolean isInputShutdown()
{
return BufferUtil.isEmpty(_decryptedInput) && (getEndPoint().isInputShutdown() || isInboundDone());
}
private boolean isInboundDone()
{
try
{
return _sslEngine.isInboundDone();
}
catch (Throwable x)
{
LOG.trace("IGNORED", x);
return true;
}
}
private void notifyHandshakeSucceeded(SSLEngine sslEngine) throws SSLException
{
SslHandshakeListener.Event event = null;
for (SslHandshakeListener listener : handshakeListeners)
{
if (event == null)
event = new SslHandshakeListener.Event(sslEngine);
try
{
listener.handshakeSucceeded(event);
}
catch (SSLException x)
{
throw x;
}
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);
}
}
}
private boolean isRenegotiating()
{
if (!isHandshakeComplete())
return false;
if (isTLS13())
return false;
if (_sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING)
return false;
return true;
}
private boolean allowRenegotiate()
{
if (!isRenegotiationAllowed())
{
if (LOG.isDebugEnabled())
LOG.debug("Renegotiation denied {}", SslConnection.this);
terminateInput();
return false;
}
if (getRenegotiationLimit() == 0)
{
if (LOG.isDebugEnabled())
LOG.debug("Renegotiation limit exceeded {}", SslConnection.this);
terminateInput();
return false;
}
return true;
}
private boolean isTLS13()
{
String protocol = _sslEngine.getSession().getProtocol();
return TLS_1_3.equals(protocol);
}
private Throwable handleException(Throwable x, String context)
{
try (AutoLock l = _lock.lock())
{
if (_failure == null)
{
_failure = x;
if (LOG.isDebugEnabled())
LOG.debug("{} stored {} exception", this, context, x);
}
else if (x != _failure)
{
_failure.addSuppressed(x);
if (LOG.isDebugEnabled())
LOG.debug("{} suppressed {} exception", this, context, x);
}
return _failure;
}
}
private void rethrow(Throwable x) throws IOException
{
if (x instanceof RuntimeException)
throw (RuntimeException)x;
if (x instanceof Error)
throw (Error)x;
if (x instanceof IOException)
throw (IOException)x;
throw new IOException(x);
}
@Override
public String toString()
{
return super.toEndPointString();
}
private final class IncompleteWriteCallback implements Callback, Invocable
{
@Override
public void succeeded()
{
boolean fillable;
boolean interested;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("IncompleteWriteCB succeeded {}", SslConnection.this);
releaseEncryptedOutputBuffer();
_flushState = FlushState.IDLE;
interested = _fillState == FillState.INTERESTED;
fillable = _fillState == FillState.WAIT_FOR_FLUSH;
if (fillable)
_fillState = FillState.IDLE;
}
if (interested)
ensureFillInterested();
else if (fillable)
_decryptedEndPoint.getFillInterest().fillable();
_decryptedEndPoint.getWriteFlusher().completeWrite();
}
@Override
public void failed(final Throwable x)
{
boolean failFillInterest;
try (AutoLock l = _lock.lock())
{
if (LOG.isDebugEnabled())
LOG.debug("IncompleteWriteCB failed {}", SslConnection.this, x);
BufferUtil.clear(_encryptedOutput);
releaseEncryptedOutputBuffer();
_flushState = FlushState.IDLE;
failFillInterest = _fillState == FillState.WAIT_FOR_FLUSH ||
_fillState == FillState.INTERESTED;
if (failFillInterest)
_fillState = FillState.IDLE;
}
getExecutor().execute(() ->
{
if (failFillInterest)
_decryptedEndPoint.getFillInterest().onFail(x);
_decryptedEndPoint.getWriteFlusher().onFail(x);
});
}
@Override
public InvocationType getInvocationType()
{
return _decryptedEndPoint.getWriteFlusher().getCallbackInvocationType();
}
@Override
public String toString()
{
return String.format("SSL@%h.DEP.writeCallback", SslConnection.this);
}
}
}
}