All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.io.ssl.SslConnection Maven / Gradle / Ivy

There is a newer version: 12.1.0.alpha0
Show newest version
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.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.SocketAddress;
import java.nio.ByteBuffer;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicLong;
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.RetainableByteBuffer;
import org.eclipse.jetty.io.WriteFlusher;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.ExceptionUtil;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.ssl.SslContextFactory;
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 {@link Connection} like HttpConnection) that
 * wants unencrypted data.
 * 

* The connector uses an {@link EndPoint} (typically SocketChannelEndPoint) as * it's source/sink of encrypted data. It then provides an endpoint via {@link #getSslEndPoint()} 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 {@link SslEndPoint#fill(ByteBuffer)} and {@link SslEndPoint#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)}. *

* It is only the active methods : {@link SslEndPoint#fillInterested(Callback)} and * {@link SslEndPoint#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. *

* 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 AtomicReference _handshake = new AtomicReference<>(HandshakeState.INITIAL); private final List handshakeListeners = new ArrayList<>(); private final AtomicLong _bytesIn = new AtomicLong(); private final AtomicLong _bytesOut = new AtomicLong(); private final ByteBufferPool _bufferPool; private final SSLEngine _sslEngine; private final SslContextFactory _sslContextFactory; private final SslEndPoint _sslEndPoint; private final boolean _encryptedDirectBuffers; private final boolean _decryptedDirectBuffers; private RetainableByteBuffer _decryptedInput; private RetainableByteBuffer _encryptedInput; private RetainableByteBuffer _encryptedOutput; private boolean _renegotiationAllowed; private int _renegotiationLimit = -1; private boolean _closedOutbound; private boolean _requireCloseMessage; private FlushState _flushState = FlushState.IDLE; private FillState _fillState = FillState.IDLE; private boolean _underflown; private final Runnable _runFillable = new RunnableTask("runFillable") { @Override public void run() { _sslEndPoint.getFillInterest().fillable(); } @Override public InvocationType getInvocationType() { return _sslEndPoint.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 getSslEndPoint().getFillInterest().getCallbackInvocationType(); } @Override public String toString() { return String.format("SSLC.NBReadCB@%x{%s}", SslConnection.this.hashCode(), SslConnection.this); } }; @Deprecated public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine) { this(byteBufferPool, executor, null, endPoint, sslEngine, false, false); } public SslConnection(ByteBufferPool byteBufferPool, Executor executor, SslContextFactory sslContextFactory, EndPoint endPoint, SSLEngine sslEngine) { this(byteBufferPool, executor, sslContextFactory, endPoint, sslEngine, false, false); } @Deprecated public SslConnection(ByteBufferPool byteBufferPool, Executor executor, EndPoint endPoint, SSLEngine sslEngine, boolean useDirectBuffersForEncryption, boolean useDirectBuffersForDecryption) { this(byteBufferPool, executor, null, endPoint, sslEngine, useDirectBuffersForEncryption, useDirectBuffersForDecryption); } public SslConnection(ByteBufferPool byteBufferPool, Executor executor, SslContextFactory sslContextFactory, 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); _bufferPool = byteBufferPool; _sslEngine = sslEngine; _sslEndPoint = newSslEndPoint(); _sslContextFactory = sslContextFactory; _encryptedDirectBuffers = useDirectBuffersForEncryption; _decryptedDirectBuffers = useDirectBuffersForDecryption; } @Override public long getBytesIn() { return _bytesIn.get(); } @Override public long getBytesOut() { return _bytesOut.get(); } public void addHandshakeListener(SslHandshakeListener listener) { handshakeListeners.add(listener); } public boolean removeHandshakeListener(SslHandshakeListener listener) { return handshakeListeners.remove(listener); } protected SslEndPoint newSslEndPoint() { return new SslEndPoint(); } public SSLEngine getSSLEngine() { return _sslEngine; } public SslEndPoint getSslEndPoint() { return _sslEndPoint; } public boolean isRenegotiationAllowed() { return _renegotiationAllowed; } public void setRenegotiationAllowed(boolean renegotiationAllowed) { _renegotiationAllowed = renegotiationAllowed; } /** * @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; } /** * @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; } /** * @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 {@code 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.

* * @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 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() { // TODO: before the output was done with the BBP only. if (_encryptedOutput == null) _encryptedOutput = _bufferPool.acquire(getPacketBufferSize(), _encryptedDirectBuffers); } @Override public void onUpgradeTo(ByteBuffer buffer) { acquireEncryptedInput(); BufferUtil.append(_encryptedInput.getByteBuffer(), buffer); } @Override public void onOpen() { super.onOpen(); getSslEndPoint().getConnection().onOpen(); } @Override public void onClose(Throwable cause) { getSslEndPoint().getConnection().onClose(cause); super.onClose(cause); } @Override public void close() { getSslEndPoint().getConnection().close(); } @Override public boolean onIdleExpired(TimeoutException timeoutException) { return getSslEndPoint().getConnection().onIdleExpired(timeoutException); } @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 (_sslEndPoint.isInputShutdown()) _sslEndPoint.close(); _sslEndPoint.onFillable(); if (LOG.isDebugEnabled()) LOG.debug("%s=>%s", getClass().getSimpleName(), hashCode(), _sslEngine.getHandshakeStatus(), ei, eo, di, _fillState, _flushState, _sslEndPoint.toEndPointString(), connection instanceof AbstractConnection ? ((AbstractConnection)connection).toConnectionString() : connection); } private void releaseEmptyEncryptedInputBuffer() { if (!_lock.isHeldByCurrentThread()) throw new IllegalStateException(); if (_encryptedInput != null && !_encryptedInput.hasRemaining()) { _encryptedInput.release(); _encryptedInput = null; } } private void releaseEmptyDecryptedInputBuffer() { if (!_lock.isHeldByCurrentThread()) throw new IllegalStateException(); if (_decryptedInput != null && !_decryptedInput.hasRemaining()) { _decryptedInput.release(); _decryptedInput = null; } } private void discardInputBuffers() { if (!_lock.isHeldByCurrentThread()) throw new IllegalStateException(); if (_encryptedInput != null) _encryptedInput.clear(); if (_decryptedInput != null) _decryptedInput.clear(); releaseEmptyInputBuffers(); } private void releaseEmptyInputBuffers() { releaseEmptyEncryptedInputBuffer(); releaseEmptyDecryptedInputBuffer(); } private void discardEncryptedOutputBuffer() { if (!_lock.isHeldByCurrentThread()) throw new IllegalStateException(); if (_encryptedOutput != null) _encryptedOutput.clear(); releaseEmptyEncryptedOutputBuffer(); } private void releaseEmptyEncryptedOutputBuffer() { if (!_lock.isHeldByCurrentThread()) throw new IllegalStateException(); if (_encryptedOutput != null && !_encryptedOutput.hasRemaining()) { _encryptedOutput.release(); _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 SslEndPoint extends AbstractEndPoint implements EndPoint.Wrapper { // This is not a simple EndPoint.Wrapper because it has another set of the machinery // from AbstractEndPoint for fillInterest and write flushing, separate to the wrapped EndPoint private final Callback _incompleteWriteCallback = new IncompleteWriteCallback(); private Throwable _failure; private SslSessionData _sslSessionData; public SslEndPoint() { // 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 SocketAddress getLocalSocketAddress() { return getEndPoint().getLocalSocketAddress(); } @Override public SocketAddress getRemoteSocketAddress() { return getEndPoint().getRemoteSocketAddress(); } @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 (_decryptedInput != null && _decryptedInput.hasRemaining()) return filled = BufferUtil.append(buffer, _decryptedInput.getByteBuffer()); // 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) throw IO.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 { _decryptedInput = _bufferPool.acquire(appBufferSize, _decryptedDirectBuffers); appIn = _decryptedInput.getByteBuffer(); } } else { appIn = _decryptedInput.getByteBuffer(); } // Let's try reading some encrypted data... even if we have some already. int netFilled = networkFill(_encryptedInput.getByteBuffer()); if (netFilled > 0) _bytesIn.addAndGet(netFilled); if (LOG.isDebugEnabled()) LOG.debug("net filled={}", netFilled); // Workaround for Java 11 behavior. if (netFilled < 0 && isHandshakeInitial() && (_encryptedInput == null || !_encryptedInput.hasRemaining())) 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.getByteBuffer(), appIn); } finally { BufferUtil.flipToFlush(appIn, pos); } if (LOG.isDebugEnabled()) LOG.debug("unwrap net_filled={} {} encryptedBuffer={} unwrapBuffer={} appBuffer={}", netFilled, StringUtil.replace(unwrapResult.toString(), '\n', ' '), _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) throw IO.rethrow(failure); return filled = -1; case BUFFER_UNDERFLOW: // Continue if we can compact? if (BufferUtil.compact(_encryptedInput.getByteBuffer())) continue; // Are we out of space? if (BufferUtil.space(_encryptedInput.getByteBuffer()) == 0) { BufferUtil.clear(_encryptedInput.getByteBuffer()); throw new SSLHandshakeException("Encrypted buffer max length exceeded"); } // if we just filled some 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.addSuppressed(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 ((_decryptedInput == null || !_decryptedInput.hasRemaining()) && appBufferSize < getApplicationBufferSize()) { releaseEmptyDecryptedInputBuffer(); 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.getByteBuffer()); } break; default: throw new IllegalStateException("Unexpected unwrap result " + unwrap); } } } catch (Throwable x) { discardInputBuffers(); Throwable f = handleException(x, "fill"); Throwable failure = handshakeFailed(f); if (_flushState == FlushState.WAIT_FOR_FILL) { _flushState = FlushState.IDLE; getExecutor().execute(() -> _sslEndPoint.getWriteFlusher().onFail(failure)); } throw failure; } finally { releaseEmptyInputBuffers(); if (_flushState == FlushState.WAIT_FOR_FILL) { _flushState = FlushState.IDLE; getExecutor().execute(() -> _sslEndPoint.getWriteFlusher().completeWrite()); } if (LOG.isDebugEnabled()) LOG.debug("needFillInterest s={}/{} uf={} ei={} di={} {}", _flushState, _fillState, _underflown, _encryptedInput, _decryptedInput, SslConnection.this); if (_fillState != FillState.IDLE) return; // Fillable if we have decrypted input OR enough encrypted input. fillable = (_decryptedInput != null && _decryptedInput.hasRemaining()) || (_encryptedInput != null && _encryptedInput.hasRemaining() && !_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 && (_encryptedOutput != null && _encryptedOutput.hasRemaining())) { _flushState = FlushState.WRITING; write = _encryptedOutput.getByteBuffer(); } } break; case NEED_WRAP: if (!fillable) { _fillState = FillState.WAIT_FOR_FLUSH; if (_flushState == FlushState.IDLE) { _flushState = FlushState.WRITING; write = (_encryptedOutput != null && _encryptedOutput.hasRemaining()) ? _encryptedOutput.getByteBuffer() : BufferUtil.EMPTY_BUFFER; } } break; default: throw new IllegalStateException("Unexpected HandshakeStatus " + status); } if (LOG.isDebugEnabled()) LOG.debug(" 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)); } } Boolean result = null; try { // finish of any previous flushes if (_encryptedOutput != null) { int remaining = _encryptedOutput.remaining(); if (remaining > 0) { boolean flushed = networkFlush(_encryptedOutput.getByteBuffer()); int written = remaining - _encryptedOutput.remaining(); if (written > 0) _bytesOut.addAndGet(written); if (!flushed) return false; } } boolean isEmpty = BufferUtil.isEmpty(appOuts); 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. ByteBuffer encryptedOutputBuffer = _encryptedOutput.getByteBuffer(); BufferUtil.compact(encryptedOutputBuffer); int pos = BufferUtil.flipToFill(encryptedOutputBuffer); SSLEngineResult wrapResult; try { wrapResult = wrap(_sslEngine, appOuts, encryptedOutputBuffer); } finally { BufferUtil.flipToFlush(encryptedOutputBuffer, pos); } if (LOG.isDebugEnabled()) LOG.debug("wrap {} {} ioDone={}/{}", StringUtil.replace(wrapResult.toString(), '\n', ' '), _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; int remaining = encryptedOutputBuffer.remaining(); if (remaining > 0) { flushed = networkFlush(encryptedOutputBuffer); int written = remaining - encryptedOutputBuffer.remaining(); if (written > 0) _bytesOut.addAndGet(written); } 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()) { releaseEmptyEncryptedOutputBuffer(); continue; } throw new IllegalStateException("Unexpected wrap result " + wrap); case OK: if (wrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED) handshakeSucceeded(); if (isRenegotiating() && !allowRenegotiate()) { getEndPoint().shutdownOutput(); if (isEmpty && (_encryptedOutput == null || !_encryptedOutput.hasRemaining())) 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) { discardEncryptedOutputBuffer(); Throwable failure = handleException(x, "flush"); throw handshakeFailed(failure); } finally { releaseEmptyEncryptedOutputBuffer(); if (LOG.isDebugEnabled()) LOG.debug("onIncompleteFlush {} {}", SslConnection.this, _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 = (_encryptedOutput != null && _encryptedOutput.hasRemaining()) ? _encryptedOutput.getByteBuffer() : 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 (_encryptedOutput != null && _encryptedOutput.hasRemaining()) { write = _encryptedOutput.getByteBuffer(); _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(" { try (AutoLock l = _lock.lock()) { _flushState = FlushState.IDLE; releaseEmptyEncryptedOutputBuffer(); } }, t -> disconnect()), write); } } } if (close) disconnect(); else ensureFillInterested(); } catch (Throwable x) { if (LOG.isTraceEnabled()) LOG.trace("IGNORED", x); disconnect(); } } private void disconnect() { try (AutoLock l = _lock.lock()) { discardEncryptedOutputBuffer(); } getEndPoint().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() { try (AutoLock l = _lock.lock()) { discardInputBuffers(); } // First send the TLS Close Alert, then the FIN. doShutdownOutput(true); super.doClose(); } @Override public Object getTransport() { return getEndPoint(); } @Override public boolean isInputShutdown() { return (_decryptedInput == null || !_decryptedInput.hasRemaining()) && (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, this); 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, this); 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; return _sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING; } 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 { ExceptionUtil.addSuppressedIfNotAssociated(_failure, x); if (LOG.isDebugEnabled()) LOG.debug("{} suppressed {} exception", this, context, x); } return _failure; } } @Override public SslSessionData getSslSessionData() { SSLSession sslSession = _sslEngine.getSession(); SslSessionData sslSessionData = _sslSessionData; if (sslSessionData == null) { String cipherSuite = sslSession.getCipherSuite(); X509Certificate[] peerCertificates = _sslContextFactory != null ? _sslContextFactory.getX509CertChain(sslSession) : SslContextFactory.getCertChain(sslSession); byte[] bytes = sslSession.getId(); String idStr = StringUtil.toHexString(bytes); sslSessionData = SslSessionData.from(sslSession, idStr, cipherSuite, peerCertificates); _sslSessionData = sslSessionData; } return sslSessionData; } @Override public String toString() { return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), 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); releaseEmptyEncryptedOutputBuffer(); _flushState = FlushState.IDLE; interested = _fillState == FillState.INTERESTED; fillable = _fillState == FillState.WAIT_FOR_FLUSH; if (fillable) _fillState = FillState.IDLE; } if (interested) ensureFillInterested(); else if (fillable) _sslEndPoint.getFillInterest().fillable(); _sslEndPoint.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); discardEncryptedOutputBuffer(); _flushState = FlushState.IDLE; failFillInterest = _fillState == FillState.WAIT_FOR_FLUSH || _fillState == FillState.INTERESTED; if (failFillInterest) _fillState = FillState.IDLE; } getExecutor().execute(() -> { if (failFillInterest) _sslEndPoint.getFillInterest().onFail(x); _sslEndPoint.getWriteFlusher().onFail(x); }); } @Override public InvocationType getInvocationType() { return _sslEndPoint.getWriteFlusher().getCallbackInvocationType(); } @Override public String toString() { return String.format("SSL@%h.DEP.writeCallback", SslConnection.this); } } } private abstract class RunnableTask implements Invocable.Task { 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()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy