/*
 * Copyright 2016 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.handler.ssl;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.util.LazyJavaxX509Certificate;
import io.netty.handler.ssl.util.LazyX509Certificate;
import io.netty.internal.tcnative.AsyncTask;
import io.netty.internal.tcnative.Buffer;
import io.netty.internal.tcnative.SSL;
import io.netty.util.AbstractReferenceCounted;
import io.netty.util.CharsetUtil;
import io.netty.util.ReferenceCounted;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetectorFactory;
import io.netty.util.ResourceLeakTracker;
import io.netty.util.internal.EmptyArrays;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.SuppressJava6Requirement;
import io.netty.util.internal.ThrowableUtil;
import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;

import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener;
import javax.security.cert.X509Certificate;

import static io.netty.handler.ssl.OpenSsl.memoryAddress;
import static io.netty.handler.ssl.SslUtils.SSL_RECORD_HEADER_LENGTH;
import static io.netty.util.internal.EmptyArrays.EMPTY_STRINGS;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkNotNullArrayParam;
import static io.netty.util.internal.ObjectUtil.checkNotNullWithIAE;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.Math.min;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_TASK;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NEED_WRAP;
import static javax.net.ssl.SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
import static javax.net.ssl.SSLEngineResult.Status.BUFFER_OVERFLOW;
import static javax.net.ssl.SSLEngineResult.Status.BUFFER_UNDERFLOW;
import static javax.net.ssl.SSLEngineResult.Status.CLOSED;
import static javax.net.ssl.SSLEngineResult.Status.OK;

/**
 * Implements a {@link SSLEngine} using
 * <a href="https://www.openssl.org/docs/crypto/BIO_s_bio.html#EXAMPLE">OpenSSL BIO abstractions</a>.
 * <p>Instances of this class must be {@link #release() released} or else native memory will leak!
 *
 * <p>Instances of this class <strong>must</strong> be released before the {@link ReferenceCountedOpenSslContext}
 * the instance depends upon are released. Otherwise if any method of this class is called which uses the
 * the {@link ReferenceCountedOpenSslContext} JNI resources the JVM may crash.
 */
public class ReferenceCountedOpenSslEngine extends SSLEngine implements ReferenceCounted, ApplicationProtocolAccessor {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(ReferenceCountedOpenSslEngine.class);

    private static final ResourceLeakDetector<ReferenceCountedOpenSslEngine> leakDetector =
            ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ReferenceCountedOpenSslEngine.class);
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2 = 0;
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3 = 1;
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1 = 2;
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1 = 3;
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2 = 4;
    private static final int OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3 = 5;
    private static final int[] OPENSSL_OP_NO_PROTOCOLS = {
            SSL.SSL_OP_NO_SSLv2,
            SSL.SSL_OP_NO_SSLv3,
            SSL.SSL_OP_NO_TLSv1,
            SSL.SSL_OP_NO_TLSv1_1,
            SSL.SSL_OP_NO_TLSv1_2,
            SSL.SSL_OP_NO_TLSv1_3
    };

    /**
     * Depends upon tcnative ... only use if tcnative is available!
     */
    static final int MAX_PLAINTEXT_LENGTH = SSL.SSL_MAX_PLAINTEXT_LENGTH;
    /**
     * Depends upon tcnative ... only use if tcnative is available!
     */
    static final int MAX_RECORD_SIZE = SSL.SSL_MAX_RECORD_LENGTH;

    private static final SSLEngineResult NEED_UNWRAP_OK = new SSLEngineResult(OK, NEED_UNWRAP, 0, 0);
    private static final SSLEngineResult NEED_UNWRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_UNWRAP, 0, 0);
    private static final SSLEngineResult NEED_WRAP_OK = new SSLEngineResult(OK, NEED_WRAP, 0, 0);
    private static final SSLEngineResult NEED_WRAP_CLOSED = new SSLEngineResult(CLOSED, NEED_WRAP, 0, 0);
    private static final SSLEngineResult CLOSED_NOT_HANDSHAKING = new SSLEngineResult(CLOSED, NOT_HANDSHAKING, 0, 0);

    // OpenSSL state
    private long ssl;
    private long networkBIO;

    private enum HandshakeState {
        /**
         * Not started yet.
         */
        NOT_STARTED,
        /**
         * Started via unwrap/wrap.
         */
        STARTED_IMPLICITLY,
        /**
         * Started via {@link #beginHandshake()}.
         */
        STARTED_EXPLICITLY,
        /**
         * Handshake is finished.
         */
        FINISHED
    }

    private HandshakeState handshakeState = HandshakeState.NOT_STARTED;
    private boolean receivedShutdown;
    private volatile boolean destroyed;
    private volatile String applicationProtocol;
    private volatile boolean needTask;
    private boolean hasTLSv13Cipher;
    private boolean sessionSet;

    // Reference Counting
    private final ResourceLeakTracker<ReferenceCountedOpenSslEngine> leak;
    private final AbstractReferenceCounted refCnt = new AbstractReferenceCounted() {
        @Override
        public ReferenceCounted touch(Object hint) {
            if (leak != null) {
                leak.record(hint);
            }

            return ReferenceCountedOpenSslEngine.this;
        }

        @Override
        protected void deallocate() {
            shutdown();
            if (leak != null) {
                boolean closed = leak.close(ReferenceCountedOpenSslEngine.this);
                assert closed;
            }
            parentContext.release();
        }
    };

    private final Set<String> enabledProtocols = new LinkedHashSet<String>();

    private volatile ClientAuth clientAuth = ClientAuth.NONE;

    private String endpointIdentificationAlgorithm;
    // Store as object as AlgorithmConstraints only exists since java 7.
    private Object algorithmConstraints;
    private List<String> sniHostNames;

    // Mark as volatile as accessed by checkSniHostnameMatch(...) and also not specify the SNIMatcher type to allow us
    // using it with java7.
    private volatile Collection<?> matchers;

    // SSL Engine status variables
    private boolean isInboundDone;
    private boolean outboundClosed;

    final boolean jdkCompatibilityMode;
    private final boolean clientMode;
    final ByteBufAllocator alloc;
    private final OpenSslEngineMap engineMap;
    private final OpenSslApplicationProtocolNegotiator apn;
    private final ReferenceCountedOpenSslContext parentContext;
    private final OpenSslInternalSession session;
    private final ByteBuffer[] singleSrcBuffer = new ByteBuffer[1];
    private final ByteBuffer[] singleDstBuffer = new ByteBuffer[1];
    private final boolean enableOcsp;
    private int maxWrapOverhead;
    private int maxWrapBufferSize;
    private Throwable pendingException;

    /**
     * Create a new instance.
     * @param context Reference count release responsibility is not transferred! The callee still owns this object.
     * @param alloc The allocator to use.
     * @param peerHost The peer host name.
     * @param peerPort The peer port.
     * @param jdkCompatibilityMode {@code true} to behave like described in
     *                             https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html.
     *                             {@code false} allows for partial and/or multiple packets to be process in a single
     *                             wrap or unwrap call.
     * @param leakDetection {@code true} to enable leak detection of this object.
     */
    ReferenceCountedOpenSslEngine(ReferenceCountedOpenSslContext context, final ByteBufAllocator alloc, String peerHost,
                                  int peerPort, boolean jdkCompatibilityMode, boolean leakDetection,
                                  String endpointIdentificationAlgorithm) {
        super(peerHost, peerPort);
        OpenSsl.ensureAvailability();
        engineMap = context.engineMap;
        enableOcsp = context.enableOcsp;
        this.jdkCompatibilityMode = jdkCompatibilityMode;
        this.alloc = checkNotNull(alloc, "alloc");
        apn = (OpenSslApplicationProtocolNegotiator) context.applicationProtocolNegotiator();
        clientMode = context.isClient();
        this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;

        if (PlatformDependent.javaVersion() >= 7) {
            session = new ExtendedOpenSslSession(new DefaultOpenSslSession(context.sessionContext())) {
                private String[] peerSupportedSignatureAlgorithms;
                private List requestedServerNames;

                @Override
                public List getRequestedServerNames() {
                    if (clientMode) {
                        return Java8SslUtils.getSniHostNames(sniHostNames);
                    } else {
                        synchronized (ReferenceCountedOpenSslEngine.this) {
                            if (requestedServerNames == null) {
                                if (isDestroyed()) {
                                    requestedServerNames = Collections.emptyList();
                                } else {
                                    String name = SSL.getSniHostname(ssl);
                                    if (name == null) {
                                        requestedServerNames = Collections.emptyList();
                                    } else {
                                        // Convert to bytes as we do not want to do any strict validation of the
                                        // SNIHostName while creating it.
                                        requestedServerNames =
                                                Java8SslUtils.getSniHostName(
                                                        SSL.getSniHostname(ssl).getBytes(CharsetUtil.UTF_8));
                                    }
                                }
                            }
                            return requestedServerNames;
                        }
                    }
                }

                @Override
                public String[] getPeerSupportedSignatureAlgorithms() {
                    synchronized (ReferenceCountedOpenSslEngine.this) {
                        if (peerSupportedSignatureAlgorithms == null) {
                            if (isDestroyed()) {
                                peerSupportedSignatureAlgorithms = EMPTY_STRINGS;
                            } else {
                                String[] algs = SSL.getSigAlgs(ssl);
                                if (algs == null) {
                                    peerSupportedSignatureAlgorithms = EMPTY_STRINGS;
                                } else {
                                    Set<String> algorithmList = new LinkedHashSet<String>(algs.length);
                                    for (String alg: algs) {
                                        String converted = SignatureAlgorithmConverter.toJavaName(alg);

                                        if (converted != null) {
                                            algorithmList.add(converted);
                                        }
                                    }
                                    peerSupportedSignatureAlgorithms = algorithmList.toArray(EMPTY_STRINGS);
                                }
                            }
                        }
                        return peerSupportedSignatureAlgorithms.clone();
                    }
                }

                @Override
                public List<byte[]> getStatusResponses() {
                    byte[] ocspResponse = null;
                    if (enableOcsp && clientMode) {
                        synchronized (ReferenceCountedOpenSslEngine.this) {
                            if (!isDestroyed()) {
                                ocspResponse = SSL.getOcspResponse(ssl);
                            }
                        }
                    }
                    return ocspResponse == null ?
                            Collections.<byte[]>emptyList() : Collections.singletonList(ocspResponse);
                }
            };
        } else {
            session = new DefaultOpenSslSession(context.sessionContext());
        }

        if (!context.sessionContext().useKeyManager()) {
            session.setLocalCertificate(context.keyCertChain);
        }

        Lock readerLock = context.ctxLock.readLock();
        readerLock.lock();
        final long finalSsl;
        try {
            finalSsl = SSL.newSSL(context.ctx, !context.isClient());
        } finally {
            readerLock.unlock();
        }
        synchronized (this) {
            ssl = finalSsl;
            try {
                networkBIO = SSL.bioNewByteBuffer(ssl, context.getBioNonApplicationBufferSize());

                // Set the client auth mode, this needs to be done via setClientAuth(...) method so we actually call the
                // needed JNI methods.
                setClientAuth(clientMode ? ClientAuth.NONE : context.clientAuth);

                assert context.protocols != null;
                this.hasTLSv13Cipher = context.hasTLSv13Cipher;

                setEnabledProtocols(context.protocols);

                // Use SNI if peerHost was specified and a valid hostname
                // See https://github.com/netty/netty/issues/4746
                if (clientMode && SslUtils.isValidHostNameForSNI(peerHost)) {
                    // If on java8 and later we should do some extra validation to ensure we can construct the
                    // SNIHostName later again.
                    if (PlatformDependent.javaVersion() >= 8) {
                        if (Java8SslUtils.isValidHostNameForSNI(peerHost)) {
                            SSL.setTlsExtHostName(ssl, peerHost);
                            sniHostNames = Collections.singletonList(peerHost);
                        }
                    } else {
                        SSL.setTlsExtHostName(ssl, peerHost);
                        sniHostNames = Collections.singletonList(peerHost);
                    }
                }

                if (enableOcsp) {
                    SSL.enableOcsp(ssl);
                }

                if (!jdkCompatibilityMode) {
                    SSL.setMode(ssl, SSL.getMode(ssl) | SSL.SSL_MODE_ENABLE_PARTIAL_WRITE);
                }

                if (isProtocolEnabled(SSL.getOptions(ssl), SSL.SSL_OP_NO_TLSv1_3, SslProtocols.TLS_v1_3)) {
                    final boolean enableTickets = clientMode ?
                            ReferenceCountedOpenSslContext.CLIENT_ENABLE_SESSION_TICKET_TLSV13 :
                            ReferenceCountedOpenSslContext.SERVER_ENABLE_SESSION_TICKET_TLSV13;
                    if (enableTickets) {
                        // We should enable session tickets for stateless resumption when TLSv1.3 is enabled. This
                        // is also done by OpenJDK and without this session resumption does not work at all with
                        // BoringSSL when TLSv1.3 is used as BoringSSL only supports stateless resumption with TLSv1.3:
                        //
                        // See:
                        //  - https://bugs.openjdk.java.net/browse/JDK-8223922
                        //  - https://boringssl.googlesource.com/boringssl/+/refs/heads/master/ssl/tls13_server.cc#104
                        SSL.clearOptions(ssl, SSL.SSL_OP_NO_TICKET);
                    }
                }

                if (OpenSsl.isBoringSSL() && clientMode) {
                    // If in client-mode and BoringSSL let's allow to renegotiate once as the server may use this
                    // for client auth.
                    //
                    // See https://github.com/netty/netty/issues/11529
                    SSL.setRenegotiateMode(ssl, SSL.SSL_RENEGOTIATE_ONCE);
                }
                // setMode may impact the overhead.
                calculateMaxWrapOverhead();

                // Configure any endpoint verification specified by the SslContext.
                configureEndpointVerification(endpointIdentificationAlgorithm);
            } catch (Throwable cause) {
                // Call shutdown so we are sure we correctly release all native memory and also guard against the
                // case when shutdown() will be called by the finalizer again.
                shutdown();

                PlatformDependent.throwException(cause);
            }
        }

        // Now that everything looks good and we're going to successfully return the
        // object so we need to retain a reference to the parent context.
        parentContext = context;
        parentContext.retain();

        // Only create the leak after everything else was executed and so ensure we don't produce a false-positive for
        // the ResourceLeakDetector.
        leak = leakDetection ? leakDetector.track(this) : null;
    }

    final synchronized String[] authMethods() {
        if (isDestroyed()) {
            return EMPTY_STRINGS;
        }
        return SSL.authenticationMethods(ssl);
    }

    final boolean setKeyMaterial(OpenSslKeyMaterial keyMaterial) throws  Exception {
        synchronized (this) {
            if (isDestroyed()) {
                return false;
            }
            SSL.setKeyMaterial(ssl, keyMaterial.certificateChainAddress(), keyMaterial.privateKeyAddress());
        }
        session.setLocalCertificate(keyMaterial.certificateChain());
        return true;
    }

    final synchronized SecretKeySpec masterKey() {
        if (isDestroyed()) {
            return null;
        }
        return new SecretKeySpec(SSL.getMasterKey(ssl), "AES");
    }

    synchronized boolean isSessionReused() {
        if (isDestroyed()) {
            return false;
        }
        return SSL.isSessionReused(ssl);
    }

    /**
     * Sets the OCSP response.
     */
    @UnstableApi
    public void setOcspResponse(byte[] response) {
        if (!enableOcsp) {
            throw new IllegalStateException("OCSP stapling is not enabled");
        }

        if (clientMode) {
            throw new IllegalStateException("Not a server SSLEngine");
        }

        synchronized (this) {
            if (!isDestroyed()) {
                SSL.setOcspResponse(ssl, response);
            }
        }
    }

    /**
     * Returns the OCSP response or {@code null} if the server didn't provide a stapled OCSP response.
     */
    @UnstableApi
    public byte[] getOcspResponse() {
        if (!enableOcsp) {
            throw new IllegalStateException("OCSP stapling is not enabled");
        }

        if (!clientMode) {
            throw new IllegalStateException("Not a client SSLEngine");
        }

        synchronized (this) {
            if (isDestroyed()) {
                return EmptyArrays.EMPTY_BYTES;
            }
            return SSL.getOcspResponse(ssl);
        }
    }

    @Override
    public final int refCnt() {
        return refCnt.refCnt();
    }

    @Override
    public final ReferenceCounted retain() {
        refCnt.retain();
        return this;
    }

    @Override
    public final ReferenceCounted retain(int increment) {
        refCnt.retain(increment);
        return this;
    }

    @Override
    public final ReferenceCounted touch() {
        refCnt.touch();
        return this;
    }

    @Override
    public final ReferenceCounted touch(Object hint) {
        refCnt.touch(hint);
        return this;
    }

    @Override
    public final boolean release() {
        return refCnt.release();
    }

    @Override
    public final boolean release(int decrement) {
        return refCnt.release(decrement);
    }

    // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier
    // java8 version we don't use @Override annotations here.
    public String getApplicationProtocol() {
        return applicationProtocol;
    }

    // These method will override the method defined by Java 8u251 and later. As we may compile with an earlier
    // java8 version we don't use @Override annotations here.
    public String getHandshakeApplicationProtocol() {
        return applicationProtocol;
    }

    @Override
    public final synchronized SSLSession getHandshakeSession() {
        // Javadocs state return value should be:
        // null if this instance is not currently handshaking, or if the current handshake has not
        // progressed far enough to create a basic SSLSession. Otherwise, this method returns the
        // SSLSession currently being negotiated.
        switch(handshakeState) {
            case NOT_STARTED:
            case FINISHED:
                return null;
            default:
                return session;
        }
    }

    /**
     * Returns the pointer to the {@code SSL} object for this {@link ReferenceCountedOpenSslEngine}.
     * Be aware that it is freed as soon as the {@link #release()} or {@link #shutdown()} methods are called.
     * At this point {@code 0} will be returned.
     */
    public final synchronized long sslPointer() {
        return ssl;
    }

    /**
     * Destroys this engine.
     */
    public final synchronized void shutdown() {
        if (!destroyed) {
            destroyed = true;
            // Let's check if engineMap is null as it could be in theory if we throw an OOME during the construction of
            // ReferenceCountedOpenSslEngine (before we assign the field). This is needed as shutdown() is called from
            // the finalizer as well.
            if (engineMap != null) {
                engineMap.remove(ssl);
            }
            SSL.freeSSL(ssl);
            ssl = networkBIO = 0;

            isInboundDone = outboundClosed = true;
        }

        // On shutdown clear all errors
        SSL.clearError();
    }

    /**
     * Write plaintext data to the OpenSSL internal BIO
     *
     * Calling this function with src.remaining == 0 is undefined.
     */
    private int writePlaintextData(final ByteBuffer src, int len) {
        final int pos = src.position();
        final int limit = src.limit();
        final int sslWrote;

        if (src.isDirect()) {
            sslWrote = SSL.writeToSSL(ssl, bufferAddress(src) + pos, len);
            if (sslWrote > 0) {
                src.position(pos + sslWrote);
            }
        } else {
            ByteBuf buf = alloc.directBuffer(len);
            try {
                src.limit(pos + len);

                buf.setBytes(0, src);
                src.limit(limit);

                sslWrote = SSL.writeToSSL(ssl, memoryAddress(buf), len);
                if (sslWrote > 0) {
                    src.position(pos + sslWrote);
                } else {
                    src.position(pos);
                }
            } finally {
                buf.release();
            }
        }
        return sslWrote;
    }

   synchronized void bioSetFd(int fd) {
        if (!isDestroyed()) {
            SSL.bioSetFd(this.ssl, fd);
        }
    }

    /**
     * Write encrypted data to the OpenSSL network BIO.
     */
    private ByteBuf writeEncryptedData(final ByteBuffer src, int len) throws SSLException {
        final int pos = src.position();
        if (src.isDirect()) {
            SSL.bioSetByteBuffer(networkBIO, bufferAddress(src) + pos, len, false);
        } else {
            final ByteBuf buf = alloc.directBuffer(len);
            try {
                final int limit = src.limit();
                src.limit(pos + len);
                buf.writeBytes(src);
                // Restore the original position and limit because we don't want to consume from `src`.
                src.position(pos);
                src.limit(limit);

                SSL.bioSetByteBuffer(networkBIO, memoryAddress(buf), len, false);
                return buf;
            } catch (Throwable cause) {
                buf.release();
                PlatformDependent.throwException(cause);
            }
        }
        return null;
    }

    /**
     * Read plaintext data from the OpenSSL internal BIO
     */
    private int readPlaintextData(final ByteBuffer dst) throws SSLException {
        final int sslRead;
        final int pos = dst.position();
        if (dst.isDirect()) {
            sslRead = SSL.readFromSSL(ssl, bufferAddress(dst) + pos, dst.limit() - pos);
            if (sslRead > 0) {
                dst.position(pos + sslRead);
            }
        } else {
            final int limit = dst.limit();
            final int len = min(maxEncryptedPacketLength0(), limit - pos);
            final ByteBuf buf = alloc.directBuffer(len);
            try {
                sslRead = SSL.readFromSSL(ssl, memoryAddress(buf), len);
                if (sslRead > 0) {
                    dst.limit(pos + sslRead);
                    buf.getBytes(buf.readerIndex(), dst);
                    dst.limit(limit);
                }
            } finally {
                buf.release();
            }
        }

        return sslRead;
    }

    /**
     * Visible only for testing!
     */
    final synchronized int maxWrapOverhead() {
        return maxWrapOverhead;
    }

    /**
     * Visible only for testing!
     */
    final synchronized int maxEncryptedPacketLength() {
        return maxEncryptedPacketLength0();
    }

    /**
     * This method is intentionally not synchronized, only use if you know you are in the EventLoop
     * thread and visibility on {@link #maxWrapOverhead} is achieved via other synchronized blocks.
     */
    final int maxEncryptedPacketLength0() {
        return maxWrapOverhead + MAX_PLAINTEXT_LENGTH;
    }

    /**
     * This method is intentionally not synchronized, only use if you know you are in the EventLoop
     * thread and visibility on {@link #maxWrapBufferSize} and {@link #maxWrapOverhead} is achieved
     * via other synchronized blocks.
     * <br>
     * Calculates the max size of a single wrap operation for the given plaintextLength and
     * numComponents.
     */
    final int calculateMaxLengthForWrap(int plaintextLength, int numComponents) {
        return (int) min(maxWrapBufferSize, plaintextLength + (long) maxWrapOverhead * numComponents);
    }

    /**
     * This method is intentionally not synchronized, only use if you know you are in the EventLoop
     * thread and visibility on {@link #maxWrapOverhead} is achieved via other synchronized blocks.
     * <br>
     * Calculates the size of the out net buf to create for the given plaintextLength and numComponents.
     * This is not related to the max size per wrap, as we can wrap chunks at a time into one out net buf.
     */
    final int calculateOutNetBufSize(int plaintextLength, int numComponents) {
        return (int) min(MAX_VALUE, plaintextLength + (long) maxWrapOverhead * numComponents);
    }

    final synchronized int sslPending() {
        return sslPending0();
    }

    /**
     * It is assumed this method is called in a synchronized block (or the constructor)!
     */
    private void calculateMaxWrapOverhead() {
        maxWrapOverhead = SSL.getMaxWrapOverhead(ssl);

        // maxWrapBufferSize must be set after maxWrapOverhead because there is a dependency on this value.
        // If jdkCompatibility mode is off we allow enough space to encrypt 16 buffers at a time. This could be
        // configurable in the future if necessary.
        maxWrapBufferSize = jdkCompatibilityMode ? maxEncryptedPacketLength0() : maxEncryptedPacketLength0() << 4;
    }

    private int sslPending0() {
        // OpenSSL has a limitation where if you call SSL_pending before the handshake is complete OpenSSL will throw a
        // "called a function you should not call" error. Using the TLS_method instead of SSLv23_method may solve this
        // issue but this API is only available in 1.1.0+ [1].
        // [1] https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_new.html
        return handshakeState != HandshakeState.FINISHED ? 0 : SSL.sslPending(ssl);
    }

    private boolean isBytesAvailableEnoughForWrap(int bytesAvailable, int plaintextLength, int numComponents) {
        return bytesAvailable - (long) maxWrapOverhead * numComponents >= plaintextLength;
    }

    @Override
    public final SSLEngineResult wrap(
            final ByteBuffer[] srcs, int offset, final int length, final ByteBuffer dst) throws SSLException {
        // Throw required runtime exceptions
        checkNotNullWithIAE(srcs, "srcs");
        checkNotNullWithIAE(dst, "dst");

        if (offset >= srcs.length || offset + length > srcs.length) {
            throw new IndexOutOfBoundsException(
                    "offset: " + offset + ", length: " + length +
                            " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))");
        }

        if (dst.isReadOnly()) {
            throw new ReadOnlyBufferException();
        }

        synchronized (this) {
            if (isOutboundDone()) {
                // All drained in the outbound buffer
                return isInboundDone() || isDestroyed() ? CLOSED_NOT_HANDSHAKING : NEED_UNWRAP_CLOSED;
            }

            int bytesProduced = 0;
            ByteBuf bioReadCopyBuf = null;
            try {
                // Setup the BIO buffer so that we directly write the encryption results into dst.
                if (dst.isDirect()) {
                    SSL.bioSetByteBuffer(networkBIO, bufferAddress(dst) + dst.position(), dst.remaining(),
                            true);
                } else {
                    bioReadCopyBuf = alloc.directBuffer(dst.remaining());
                    SSL.bioSetByteBuffer(networkBIO, memoryAddress(bioReadCopyBuf), bioReadCopyBuf.writableBytes(),
                            true);
                }

                int bioLengthBefore = SSL.bioLengthByteBuffer(networkBIO);

                // Explicitly use outboundClosed as we want to drain any bytes that are still present.
                if (outboundClosed) {
                    // If the outbound was closed we want to ensure we can produce the alert to the destination buffer.
                    // This is true even if we not using jdkCompatibilityMode.
                    //
                    // We use a plaintextLength of 2 as we at least want to have an alert fit into it.
                    // https://tools.ietf.org/html/rfc5246#section-7.2
                    if (!isBytesAvailableEnoughForWrap(dst.remaining(), 2, 1)) {
                        return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0);
                    }

                    // There is something left to drain.
                    // See https://github.com/netty/netty/issues/6260
                    bytesProduced = SSL.bioFlushByteBuffer(networkBIO);
                    if (bytesProduced <= 0) {
                        return newResultMayFinishHandshake(NOT_HANDSHAKING, 0, 0);
                    }
                    // It is possible when the outbound was closed there was not enough room in the non-application
                    // buffers to hold the close_notify. We should keep trying to close until we consume all the data
                    // OpenSSL can give us.
                    if (!doSSLShutdown()) {
                        return newResultMayFinishHandshake(NOT_HANDSHAKING, 0, bytesProduced);
                    }
                    bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO);
                    return newResultMayFinishHandshake(NEED_WRAP, 0, bytesProduced);
                }

                // Flush any data that may be implicitly generated by OpenSSL (handshake, close, etc..).
                SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING;
                HandshakeState oldHandshakeState = handshakeState;

                // Prepare OpenSSL to work in server mode and receive handshake
                if (handshakeState != HandshakeState.FINISHED) {
                    if (handshakeState != HandshakeState.STARTED_EXPLICITLY) {
                        // Update accepted so we know we triggered the handshake via wrap
                        handshakeState = HandshakeState.STARTED_IMPLICITLY;
                    }

                    // Flush any data that may have been written implicitly during the handshake by OpenSSL.
                    bytesProduced = SSL.bioFlushByteBuffer(networkBIO);

                    if (pendingException != null) {
                        // TODO(scott): It is possible that when the handshake failed there was not enough room in the
                        // non-application buffers to hold the alert. We should get all the data before progressing on.
                        // However I'm not aware of a way to do this with the OpenSSL APIs.
                        // See https://github.com/netty/netty/issues/6385.

                        // We produced / consumed some data during the handshake, signal back to the caller.
                        // If there is a handshake exception and we have produced data, we should send the data before
                        // we allow handshake() to throw the handshake exception.
                        //
                        // When the user calls wrap() again we will propagate the handshake error back to the user as
                        // soon as there is no more data to was produced (as part of an alert etc).
                        if (bytesProduced > 0) {
                            return newResult(NEED_WRAP, 0, bytesProduced);
                        }
                        // Nothing was produced see if there is a handshakeException that needs to be propagated
                        // to the caller by calling handshakeException() which will return the right HandshakeStatus
                        // if it can "recover" from the exception for now.
                        return newResult(handshakeException(), 0, 0);
                    }

                    status = handshake();

                    // Handshake may have generated more data, for example if the internal SSL buffer is small
                    // we may have freed up space by flushing above.
                    bytesProduced = bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO);

                    if (status == NEED_TASK) {
                        return newResult(status, 0, bytesProduced);
                    }

                    if (bytesProduced > 0) {
                        // If we have filled up the dst buffer and we have not finished the handshake we should try to
                        // wrap again. Otherwise we should only try to wrap again if there is still data pending in
                        // SSL buffers.
                        return newResult(mayFinishHandshake(status != FINISHED ?
                                         bytesProduced == bioLengthBefore ? NEED_WRAP :
                                         getHandshakeStatus(SSL.bioLengthNonApplication(networkBIO)) : FINISHED),
                                         0, bytesProduced);
                    }

                    if (status == NEED_UNWRAP) {
                        // Signal if the outbound is done or not.
                        return isOutboundDone() ? NEED_UNWRAP_CLOSED : NEED_UNWRAP_OK;
                    }

                    // Explicit use outboundClosed and not outboundClosed() as we want to drain any bytes that are
                    // still present.
                    if (outboundClosed) {
                        bytesProduced = SSL.bioFlushByteBuffer(networkBIO);
                        return newResultMayFinishHandshake(status, 0, bytesProduced);
                    }
                }

                final int endOffset = offset + length;
                if (jdkCompatibilityMode ||
                        // If the handshake was not finished before we entered the method, we also ensure we only
                        // wrap one record. We do this to ensure we not produce any extra data before the caller
                        // of the method is able to observe handshake completion and react on it.
                        oldHandshakeState != HandshakeState.FINISHED) {
                    int srcsLen = 0;
                    for (int i = offset; i < endOffset; ++i) {
                        final ByteBuffer src = srcs[i];
                        if (src == null) {
                            throw new IllegalArgumentException("srcs[" + i + "] is null");
                        }
                        if (srcsLen == MAX_PLAINTEXT_LENGTH) {
                            continue;
                        }

                        srcsLen += src.remaining();
                        if (srcsLen > MAX_PLAINTEXT_LENGTH || srcsLen < 0) {
                            // If srcLen > MAX_PLAINTEXT_LENGTH or secLen < 0 just set it to MAX_PLAINTEXT_LENGTH.
                            // This also help us to guard against overflow.
                            // We not break out here as we still need to check for null entries in srcs[].
                            srcsLen = MAX_PLAINTEXT_LENGTH;
                        }
                    }

                    // jdkCompatibilityMode will only produce a single TLS packet, and we don't aggregate src buffers,
                    // so we always fix the number of buffers to 1 when checking if the dst buffer is large enough.
                    if (!isBytesAvailableEnoughForWrap(dst.remaining(), srcsLen, 1)) {
                        return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0);
                    }
                }

                // There was no pending data in the network BIO -- encrypt any application data
                int bytesConsumed = 0;
                assert bytesProduced == 0;

                // Flush any data that may have been written implicitly by OpenSSL in case a shutdown/alert occurs.
                bytesProduced = SSL.bioFlushByteBuffer(networkBIO);

                if (bytesProduced > 0) {
                    return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced);
                }
                // There was a pending exception that we just delayed because there was something to produce left.
                // Throw it now and shutdown the engine.
                if (pendingException != null) {
                    Throwable error = pendingException;
                    pendingException = null;
                    shutdown();
                    // Throw a new exception wrapping the pending exception, so the stacktrace is meaningful and
                    // contains all the details.
                    throw new SSLException(error);
                }

                for (; offset < endOffset; ++offset) {
                    final ByteBuffer src = srcs[offset];
                    final int remaining = src.remaining();
                    if (remaining == 0) {
                        continue;
                    }

                    final int bytesWritten;
                    if (jdkCompatibilityMode) {
                        // Write plaintext application data to the SSL engine. We don't have to worry about checking
                        // if there is enough space if jdkCompatibilityMode because we only wrap at most
                        // MAX_PLAINTEXT_LENGTH and we loop over the input before hand and check if there is space.
                        bytesWritten = writePlaintextData(src, min(remaining, MAX_PLAINTEXT_LENGTH - bytesConsumed));
                    } else {
                        // OpenSSL's SSL_write keeps state between calls. We should make sure the amount we attempt to
                        // write is guaranteed to succeed so we don't have to worry about keeping state consistent
                        // between calls.
                        final int availableCapacityForWrap = dst.remaining() - bytesProduced - maxWrapOverhead;
                        if (availableCapacityForWrap <= 0) {
                            return new SSLEngineResult(BUFFER_OVERFLOW, getHandshakeStatus(), bytesConsumed,
                                    bytesProduced);
                        }
                        bytesWritten = writePlaintextData(src, min(remaining, availableCapacityForWrap));
                    }

                    // Determine how much encrypted data was generated.
                    //
                    // Even if SSL_write doesn't consume any application data it is possible that OpenSSL will
                    // produce non-application data into the BIO. For example session tickets....
                    // See https://github.com/netty/netty/issues/10041
                    final int pendingNow = SSL.bioLengthByteBuffer(networkBIO);
                    bytesProduced += bioLengthBefore - pendingNow;
                    bioLengthBefore = pendingNow;

                    if (bytesWritten > 0) {
                        bytesConsumed += bytesWritten;

                        if (jdkCompatibilityMode || bytesProduced == dst.remaining()) {
                            return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced);
                        }
                    } else {
                        int sslError = SSL.getError(ssl, bytesWritten);
                        if (sslError == SSL.SSL_ERROR_ZERO_RETURN) {
                            // This means the connection was shutdown correctly, close inbound and outbound
                            if (!receivedShutdown) {
                                closeAll();

                                bytesProduced += bioLengthBefore - SSL.bioLengthByteBuffer(networkBIO);

                                // If we have filled up the dst buffer and we have not finished the handshake we should
                                // try to wrap again. Otherwise we should only try to wrap again if there is still data
                                // pending in SSL buffers.
                                SSLEngineResult.HandshakeStatus hs = mayFinishHandshake(
                                        status != FINISHED ? bytesProduced == dst.remaining() ? NEED_WRAP
                                                : getHandshakeStatus(SSL.bioLengthNonApplication(networkBIO))
                                                : FINISHED);
                                return newResult(hs, bytesConsumed, bytesProduced);
                            }

                            return newResult(NOT_HANDSHAKING, bytesConsumed, bytesProduced);
                        } else if (sslError == SSL.SSL_ERROR_WANT_READ) {
                            // If there is no pending data to read from BIO we should go back to event loop and try
                            // to read more data [1]. It is also possible that event loop will detect the socket has
                            // been closed. [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html
                            return newResult(NEED_UNWRAP, bytesConsumed, bytesProduced);
                        } else if (sslError == SSL.SSL_ERROR_WANT_WRITE) {
                            // SSL_ERROR_WANT_WRITE typically means that the underlying transport is not writable
                            // and we should set the "want write" flag on the selector and try again when the
                            // underlying transport is writable [1]. However we are not directly writing to the
                            // underlying transport and instead writing to a BIO buffer. The OpenSsl documentation
                            // says we should do the following [1]:
                            //
                            // "When using a buffering BIO, like a BIO pair, data must be written into or retrieved
                            // out of the BIO before being able to continue."
                            //
                            // In practice this means the destination buffer doesn't have enough space for OpenSSL
                            // to write encrypted data to. This is an OVERFLOW condition.
                            // [1] https://www.openssl.org/docs/manmaster/ssl/SSL_write.html
                            if (bytesProduced > 0) {
                                // If we produced something we should report this back and let the user call
                                // wrap again.
                                return newResult(NEED_WRAP, bytesConsumed, bytesProduced);
                            }
                            return newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced);
                        } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
                                sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
                                sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {

                            return newResult(NEED_TASK, bytesConsumed, bytesProduced);
                        } else {
                            // Everything else is considered as error
                            throw shutdownWithError("SSL_write", sslError, SSL.getLastErrorNumber());
                        }
                    }
                }
                return newResultMayFinishHandshake(status, bytesConsumed, bytesProduced);
            } finally {
                SSL.bioClearByteBuffer(networkBIO);
                if (bioReadCopyBuf == null) {
                    dst.position(dst.position() + bytesProduced);
                } else {
                    assert bioReadCopyBuf.readableBytes() <= dst.remaining() : "The destination buffer " + dst +
                            " didn't have enough remaining space to hold the encrypted content in " + bioReadCopyBuf;
                    dst.put(bioReadCopyBuf.internalNioBuffer(bioReadCopyBuf.readerIndex(), bytesProduced));
                    bioReadCopyBuf.release();
                }
            }
        }
    }

    private SSLEngineResult newResult(SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) {
        return newResult(OK, hs, bytesConsumed, bytesProduced);
    }

    private SSLEngineResult newResult(SSLEngineResult.Status status, SSLEngineResult.HandshakeStatus hs,
                                      int bytesConsumed, int bytesProduced) {
        // If isOutboundDone, then the data from the network BIO
        // was the close_notify message and all was consumed we are not required to wait
        // for the receipt the peer's close_notify message -- shutdown.
        if (isOutboundDone()) {
            if (isInboundDone()) {
                // If the inbound was done as well, we need to ensure we return NOT_HANDSHAKING to signal we are done.
                hs = NOT_HANDSHAKING;

                // As the inbound and the outbound is done we can shutdown the engine now.
                shutdown();
            }
            return new SSLEngineResult(CLOSED, hs, bytesConsumed, bytesProduced);
        }
        if (hs == NEED_TASK) {
            // Set needTask to true so getHandshakeStatus() will return the correct value.
            needTask = true;
        }
        return new SSLEngineResult(status, hs, bytesConsumed, bytesProduced);
    }

    private SSLEngineResult newResultMayFinishHandshake(SSLEngineResult.HandshakeStatus hs,
                                                        int bytesConsumed, int bytesProduced) throws SSLException {
        return newResult(mayFinishHandshake(hs, bytesConsumed, bytesProduced), bytesConsumed, bytesProduced);
    }

    private SSLEngineResult newResultMayFinishHandshake(SSLEngineResult.Status status,
                                                        SSLEngineResult.HandshakeStatus hs,
                                                        int bytesConsumed, int bytesProduced) throws SSLException {
        return newResult(status, mayFinishHandshake(hs, bytesConsumed, bytesProduced), bytesConsumed, bytesProduced);
    }

    /**
     * Log the error, shutdown the engine and throw an exception.
     */
    private SSLException shutdownWithError(String operation, int sslError, int error) {
        if (logger.isDebugEnabled()) {
            String errorString = SSL.getErrorString(error);
            logger.debug("{} failed with {}: OpenSSL error: {} {}",
                         operation, sslError, error, errorString);
        }

        // There was an internal error -- shutdown
        shutdown();

        SSLException exception = newSSLExceptionForError(error);
        // If we have a pendingException stored already we should include it as well to help the user debug things.
        if (pendingException != null) {
            exception.initCause(pendingException);
            pendingException = null;
        }
        return exception;
    }

    private SSLEngineResult handleUnwrapException(int bytesConsumed, int bytesProduced, SSLException e)
            throws SSLException {
        int lastError = SSL.getLastErrorNumber();
        if (lastError != 0) {
            return sslReadErrorResult(SSL.SSL_ERROR_SSL, lastError, bytesConsumed,
                    bytesProduced);
        }
        throw e;
    }

    public final SSLEngineResult unwrap(
            final ByteBuffer[] srcs, int srcsOffset, final int srcsLength,
            final ByteBuffer[] dsts, int dstsOffset, final int dstsLength) throws SSLException {

        // Throw required runtime exceptions
        checkNotNullWithIAE(srcs, "srcs");
        if (srcsOffset >= srcs.length
                || srcsOffset + srcsLength > srcs.length) {
            throw new IndexOutOfBoundsException(
                    "offset: " + srcsOffset + ", length: " + srcsLength +
                            " (expected: offset <= offset + length <= srcs.length (" + srcs.length + "))");
        }
        checkNotNullWithIAE(dsts, "dsts");
        if (dstsOffset >= dsts.length || dstsOffset + dstsLength > dsts.length) {
            throw new IndexOutOfBoundsException(
                    "offset: " + dstsOffset + ", length: " + dstsLength +
                            " (expected: offset <= offset + length <= dsts.length (" + dsts.length + "))");
        }
        long capacity = 0;
        final int dstsEndOffset = dstsOffset + dstsLength;
        for (int i = dstsOffset; i < dstsEndOffset; i ++) {
            ByteBuffer dst = checkNotNullArrayParam(dsts[i], i, "dsts");
            if (dst.isReadOnly()) {
                throw new ReadOnlyBufferException();
            }
            capacity += dst.remaining();
        }

        final int srcsEndOffset = srcsOffset + srcsLength;
        long len = 0;
        for (int i = srcsOffset; i < srcsEndOffset; i++) {
            ByteBuffer src = checkNotNullArrayParam(srcs[i], i, "srcs");
            len += src.remaining();
        }

        synchronized (this) {
            if (isInboundDone()) {
                return isOutboundDone() || isDestroyed() ? CLOSED_NOT_HANDSHAKING : NEED_WRAP_CLOSED;
            }

            SSLEngineResult.HandshakeStatus status = NOT_HANDSHAKING;
            HandshakeState oldHandshakeState = handshakeState;
            // Prepare OpenSSL to work in server mode and receive handshake
            if (handshakeState != HandshakeState.FINISHED) {
                if (handshakeState != HandshakeState.STARTED_EXPLICITLY) {
                    // Update accepted so we know we triggered the handshake via wrap
                    handshakeState = HandshakeState.STARTED_IMPLICITLY;
                }

                status = handshake();

                if (status == NEED_TASK) {
                    return newResult(status, 0, 0);
                }

                if (status == NEED_WRAP) {
                    return NEED_WRAP_OK;
                }
                // Check if the inbound is considered to be closed if so let us try to wrap again.
                if (isInboundDone) {
                    return NEED_WRAP_CLOSED;
                }
            }

            int sslPending = sslPending0();
            int packetLength;
            // The JDK implies that only a single SSL packet should be processed per unwrap call [1]. If we are in
            // JDK compatibility mode then we should honor this, but if not we just wrap as much as possible. If there
            // are multiple records or partial records this may reduce thrashing events through the pipeline.
            // [1] https://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLEngine.html
            if (jdkCompatibilityMode ||
                    // If the handshake was not finished before we entered the method, we also ensure we only
                    // unwrap one record. We do this to ensure we not produce any extra data before the caller
                    // of the method is able to observe handshake completion and react on it.
                    oldHandshakeState != HandshakeState.FINISHED) {
                if (len < SSL_RECORD_HEADER_LENGTH) {
                    return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0);
                }

                packetLength = SslUtils.getEncryptedPacketLength(srcs, srcsOffset);
                if (packetLength == SslUtils.NOT_ENCRYPTED) {
                    throw new NotSslRecordException("not an SSL/TLS record");
                }

                assert packetLength >= 0;

                final int packetLengthDataOnly = packetLength - SSL_RECORD_HEADER_LENGTH;
                if (packetLengthDataOnly > capacity) {
                    // Not enough space in the destination buffer so signal the caller that the buffer needs to be
                    // increased.
                    if (packetLengthDataOnly > MAX_RECORD_SIZE) {
                        // The packet length MUST NOT exceed 2^14 [1]. However we do accommodate more data to support
                        // legacy use cases which may violate this condition (e.g. OpenJDK's SslEngineImpl). If the max
                        // length is exceeded we fail fast here to avoid an infinite loop due to the fact that we
                        // won't allocate a buffer large enough.
                        // [1] https://tools.ietf.org/html/rfc5246#section-6.2.1
                        throw new SSLException("Illegal packet length: " + packetLengthDataOnly + " > " +
                                                session.getApplicationBufferSize());
                    } else {
                        session.tryExpandApplicationBufferSize(packetLengthDataOnly);
                    }
                    return newResultMayFinishHandshake(BUFFER_OVERFLOW, status, 0, 0);
                }

                if (len < packetLength) {
                    // We either don't have enough data to read the packet length or not enough for reading the whole
                    // packet.
                    return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0);
                }
            } else if (len == 0 && sslPending <= 0) {
                return newResultMayFinishHandshake(BUFFER_UNDERFLOW, status, 0, 0);
            } else if (capacity == 0) {
                return newResultMayFinishHandshake(BUFFER_OVERFLOW, status, 0, 0);
            } else {
                packetLength = (int) min(MAX_VALUE, len);
            }

            // This must always be the case when we reached here as if not we returned BUFFER_UNDERFLOW.
            assert srcsOffset < srcsEndOffset;

            // This must always be the case if we reached here.
            assert capacity > 0;

            // Number of produced bytes
            int bytesProduced = 0;
            int bytesConsumed = 0;
            try {
                srcLoop:
                for (;;) {
                    ByteBuffer src = srcs[srcsOffset];
                    int remaining = src.remaining();
                    final ByteBuf bioWriteCopyBuf;
                    int pendingEncryptedBytes;
                    if (remaining == 0) {
                        if (sslPending <= 0) {
                            // We must skip empty buffers as BIO_write will return 0 if asked to write something
                            // with length 0.
                            if (++srcsOffset >= srcsEndOffset) {
                                break;
                            }
                            continue;
                        } else {
                            bioWriteCopyBuf = null;
                            pendingEncryptedBytes = SSL.bioLengthByteBuffer(networkBIO);
                        }
                    } else {
                        // Write more encrypted data into the BIO. Ensure we only read one packet at a time as
                        // stated in the SSLEngine javadocs.
                        pendingEncryptedBytes = min(packetLength, remaining);
                        try {
                            bioWriteCopyBuf = writeEncryptedData(src, pendingEncryptedBytes);
                        } catch (SSLException e) {
                            // Ensure we correctly handle the error stack.
                            return handleUnwrapException(bytesConsumed, bytesProduced, e);
                        }
                    }
                    try {
                        for (;;) {
                            ByteBuffer dst = dsts[dstsOffset];
                            if (!dst.hasRemaining()) {
                                // No space left in the destination buffer, skip it.
                                if (++dstsOffset >= dstsEndOffset) {
                                    break srcLoop;
                                }
                                continue;
                            }

                            int bytesRead;
                            try {
                                bytesRead = readPlaintextData(dst);
                            } catch (SSLException e) {
                                // Ensure we correctly handle the error stack.
                                return handleUnwrapException(bytesConsumed, bytesProduced, e);
                            }
                            // We are directly using the ByteBuffer memory for the write, and so we only know what has
                            // been consumed after we let SSL decrypt the data. At this point we should update the
                            // number of bytes consumed, update the ByteBuffer position, and release temp ByteBuf.
                            int localBytesConsumed = pendingEncryptedBytes - SSL.bioLengthByteBuffer(networkBIO);
                            bytesConsumed += localBytesConsumed;
                            packetLength -= localBytesConsumed;
                            pendingEncryptedBytes -= localBytesConsumed;
                            src.position(src.position() + localBytesConsumed);

                            if (bytesRead > 0) {
                                bytesProduced += bytesRead;

                                if (!dst.hasRemaining()) {
                                    sslPending = sslPending0();
                                    // Move to the next dst buffer as this one is full.
                                    if (++dstsOffset >= dstsEndOffset) {
                                        return sslPending > 0 ?
                                                newResult(BUFFER_OVERFLOW, status, bytesConsumed, bytesProduced) :
                                                newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status,
                                                        bytesConsumed, bytesProduced);
                                    }
                                } else if (packetLength == 0 || jdkCompatibilityMode) {
                                    // We either consumed all data or we are in jdkCompatibilityMode and have consumed
                                    // a single TLS packet and should stop consuming until this method is called again.
                                    break srcLoop;
                                }
                            } else {
                                int sslError = SSL.getError(ssl, bytesRead);
                                if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) {
                                    // break to the outer loop as we want to read more data which means we need to
                                    // write more to the BIO.
                                    break;
                                } else if (sslError == SSL.SSL_ERROR_ZERO_RETURN) {
                                    // This means the connection was shutdown correctly, close inbound and outbound
                                    if (!receivedShutdown) {
                                        closeAll();
                                    }
                                    return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status,
                                            bytesConsumed, bytesProduced);
                                } else if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
                                        sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
                                        sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {
                                    return newResult(isInboundDone() ? CLOSED : OK,
                                            NEED_TASK, bytesConsumed, bytesProduced);
                                } else {
                                    return sslReadErrorResult(sslError, SSL.getLastErrorNumber(), bytesConsumed,
                                            bytesProduced);
                                }
                            }
                        }

                        if (++srcsOffset >= srcsEndOffset) {
                            break;
                        }
                    } finally {
                        if (bioWriteCopyBuf != null) {
                            bioWriteCopyBuf.release();
                        }
                    }
                }
            } finally {
                SSL.bioClearByteBuffer(networkBIO);
                rejectRemoteInitiatedRenegotiation();
            }

            // Check to see if we received a close_notify message from the peer.
            if (!receivedShutdown && (SSL.getShutdown(ssl) & SSL.SSL_RECEIVED_SHUTDOWN) == SSL.SSL_RECEIVED_SHUTDOWN) {
                closeAll();
            }

            return newResultMayFinishHandshake(isInboundDone() ? CLOSED : OK, status, bytesConsumed, bytesProduced);
        }
    }

    private boolean needWrapAgain(int stackError) {
        // Check if we have a pending handshakeException and if so see if we need to consume all pending data from the
        // BIO first or can just shutdown and throw it now.
        // This is needed so we ensure close_notify etc is correctly send to the remote peer.
        // See https://github.com/netty/netty/issues/3900
        if (SSL.bioLengthNonApplication(networkBIO) > 0) {
            // we seem to have data left that needs to be transferred and so the user needs
            // call wrap(...). Store the error so we can pick it up later.
            if (pendingException == null) {
                pendingException = newSSLExceptionForError(stackError);
            } else if (shouldAddSuppressed(pendingException, stackError)) {
                ThrowableUtil.addSuppressed(pendingException, newSSLExceptionForError(stackError));
            }
            // We need to clear all errors so we not pick up anything that was left on the stack on the next
            // operation. Note that shutdownWithError(...) will cleanup the stack as well so its only needed here.
            SSL.clearError();
            return true;
        }
        return false;
    }

    private SSLException newSSLExceptionForError(int stackError) {
        String message = SSL.getErrorString(stackError);
        return handshakeState == HandshakeState.FINISHED ?
                new OpenSslException(message, stackError) : new OpenSslHandshakeException(message, stackError);
    }

    private static boolean shouldAddSuppressed(Throwable target, int errorCode) {
        for (Throwable suppressed: ThrowableUtil.getSuppressed(target)) {
            if (suppressed instanceof NativeSslException &&
                    ((NativeSslException) suppressed).errorCode() == errorCode) {
                /// An exception with this errorCode was already added before.
                return false;
            }
        }
        return true;
    }

    private SSLEngineResult sslReadErrorResult(int error, int stackError, int bytesConsumed, int bytesProduced)
            throws SSLException {
        if (needWrapAgain(stackError)) {
            // There is something that needs to be send to the remote peer before we can teardown.
            // This is most likely some alert.
            return new SSLEngineResult(OK, NEED_WRAP, bytesConsumed, bytesProduced);
        }
        throw shutdownWithError("SSL_read", error, stackError);
    }

    private void closeAll() throws SSLException {
        receivedShutdown = true;
        closeOutbound();
        closeInbound();
    }

    private void rejectRemoteInitiatedRenegotiation() throws SSLHandshakeException {
        // As rejectRemoteInitiatedRenegotiation() is called in a finally block we also need to check if we shutdown
        // the engine before as otherwise SSL.getHandshakeCount(ssl) will throw an NPE if the passed in ssl is 0.
        // See https://github.com/netty/netty/issues/7353
        if (!isDestroyed() && (!clientMode && SSL.getHandshakeCount(ssl) > 1 ||
                // Let's allow to renegotiate once for client auth.
                clientMode && SSL.getHandshakeCount(ssl) > 2) &&
            // As we may count multiple handshakes when TLSv1.3 is used we should just ignore this here as
            // renegotiation is not supported in TLSv1.3 as per spec.
            !SslProtocols.TLS_v1_3.equals(session.getProtocol()) && handshakeState == HandshakeState.FINISHED) {
            // TODO: In future versions me may also want to send a fatal_alert to the client and so notify it
            // that the renegotiation failed.
            shutdown();
            throw new SSLHandshakeException("remote-initiated renegotiation not allowed");
        }
    }

    public final SSLEngineResult unwrap(final ByteBuffer[] srcs, final ByteBuffer[] dsts) throws SSLException {
        return unwrap(srcs, 0, srcs.length, dsts, 0, dsts.length);
    }

    private ByteBuffer[] singleSrcBuffer(ByteBuffer src) {
        singleSrcBuffer[0] = src;
        return singleSrcBuffer;
    }

    private void resetSingleSrcBuffer() {
        singleSrcBuffer[0] = null;
    }

    private ByteBuffer[] singleDstBuffer(ByteBuffer src) {
        singleDstBuffer[0] = src;
        return singleDstBuffer;
    }

    private void resetSingleDstBuffer() {
        singleDstBuffer[0] = null;
    }

    @Override
    public final synchronized SSLEngineResult unwrap(
            final ByteBuffer src, final ByteBuffer[] dsts, final int offset, final int length) throws SSLException {
        try {
            return unwrap(singleSrcBuffer(src), 0, 1, dsts, offset, length);
        } finally {
            resetSingleSrcBuffer();
        }
    }

    @Override
    public final synchronized SSLEngineResult wrap(ByteBuffer src, ByteBuffer dst) throws SSLException {
        try {
            return wrap(singleSrcBuffer(src), dst);
        } finally {
            resetSingleSrcBuffer();
        }
    }

    @Override
    public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer dst) throws SSLException {
        try {
            return unwrap(singleSrcBuffer(src), singleDstBuffer(dst));
        } finally {
            resetSingleSrcBuffer();
            resetSingleDstBuffer();
        }
    }

    @Override
    public final synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts) throws SSLException {
        try {
            return unwrap(singleSrcBuffer(src), dsts);
        } finally {
            resetSingleSrcBuffer();
        }
    }

    private class TaskDecorator<R extends Runnable> implements Runnable {
        protected final R task;
        TaskDecorator(R task) {
            this.task = task;
        }

        @Override
        public void run() {
            runAndResetNeedTask(task);
        }
    }

    private final class AsyncTaskDecorator extends TaskDecorator<AsyncTask> implements AsyncRunnable {
        AsyncTaskDecorator(AsyncTask task) {
            super(task);
        }

        @Override
        public void run(final Runnable runnable) {
            if (isDestroyed()) {
                // The engine was destroyed in the meantime, just return.
                return;
            }
            task.runAsync(new TaskDecorator<Runnable>(runnable));
        }
    }

    private void runAndResetNeedTask(Runnable task) {
        // We need to synchronize on the ReferenceCountedOpenSslEngine, we are sure the SSL object
        // will not be freed by the user calling for example shutdown() concurrently.
        synchronized (ReferenceCountedOpenSslEngine.this) {
            try {
                if (isDestroyed()) {
                    // The engine was destroyed in the meantime, just return.
                    return;
                }
                task.run();
                if (handshakeState != HandshakeState.FINISHED && !isDestroyed()) {
                    // Call SSL.doHandshake(...) If the handshake was not finished yet. This might be needed
                    // to fill the application buffer and so have getHandshakeStatus() return the right value
                    // in this case.
                    if (SSL.doHandshake(ssl) <= 0) {
                        SSL.clearError();
                    }
                }
            } finally {
                // The task was run, reset needTask to false so getHandshakeStatus() returns the correct value.
                needTask = false;
            }
        }
    }

    @Override
    public final synchronized Runnable getDelegatedTask() {
        if (isDestroyed()) {
            return null;
        }
        final Runnable task = SSL.getTask(ssl);
        if (task == null) {
            return null;
        }
        if (task instanceof AsyncTask) {
            return new AsyncTaskDecorator((AsyncTask) task);
        }
        return new TaskDecorator<Runnable>(task);
    }

    @Override
    public final synchronized void closeInbound() throws SSLException {
        if (isInboundDone) {
            return;
        }

        isInboundDone = true;

        if (isOutboundDone()) {
            // Only call shutdown if there is no outbound data pending.
            // See https://github.com/netty/netty/issues/6167
            shutdown();
        }

        if (handshakeState != HandshakeState.NOT_STARTED && !receivedShutdown) {
            throw new SSLException(
                    "Inbound closed before receiving peer's close_notify: possible truncation attack?");
        }
    }

    @Override
    public final synchronized boolean isInboundDone() {
        return isInboundDone;
    }

    @Override
    public final synchronized void closeOutbound() {
        if (outboundClosed) {
            return;
        }

        outboundClosed = true;

        if (handshakeState != HandshakeState.NOT_STARTED && !isDestroyed()) {
            int mode = SSL.getShutdown(ssl);
            if ((mode & SSL.SSL_SENT_SHUTDOWN) != SSL.SSL_SENT_SHUTDOWN) {
                doSSLShutdown();
            }
        } else {
            // engine closing before initial handshake
            shutdown();
        }
    }

    /**
     * Attempt to call {@link SSL#shutdownSSL(long)}.
     * @return {@code false} if the call to {@link SSL#shutdownSSL(long)} was not attempted or returned an error.
     */
    private boolean doSSLShutdown() {
        if (SSL.isInInit(ssl) != 0) {
            // Only try to call SSL_shutdown if we are not in the init state anymore.
            // Otherwise we will see 'error:140E0197:SSL routines:SSL_shutdown:shutdown while in init' in our logs.
            //
            // See also https://hg.nginx.org/nginx/rev/062c189fee20
            return false;
        }
        int err = SSL.shutdownSSL(ssl);
        if (err < 0) {
            int sslErr = SSL.getError(ssl, err);
            if (sslErr == SSL.SSL_ERROR_SYSCALL || sslErr == SSL.SSL_ERROR_SSL) {
                if (logger.isDebugEnabled()) {
                    int error = SSL.getLastErrorNumber();
                    logger.debug("SSL_shutdown failed: OpenSSL error: {} {}", error, SSL.getErrorString(error));
                }
                // There was an internal error -- shutdown
                shutdown();
                return false;
            }
            SSL.clearError();
        }
        return true;
    }

    @Override
    public final synchronized boolean isOutboundDone() {
        // Check if there is anything left in the outbound buffer.
        // We need to ensure we only call SSL.pendingWrittenBytesInBIO(...) if the engine was not destroyed yet.
        return outboundClosed && (networkBIO == 0 || SSL.bioLengthNonApplication(networkBIO) == 0);
    }

    @Override
    public final String[] getSupportedCipherSuites() {
        return OpenSsl.AVAILABLE_CIPHER_SUITES.toArray(EMPTY_STRINGS);
    }

    @Override
    public final String[] getEnabledCipherSuites() {
        final String[] extraCiphers;
        final String[] enabled;
        final boolean tls13Enabled;
        synchronized (this) {
            if (!isDestroyed()) {
                enabled = SSL.getCiphers(ssl);
                int opts = SSL.getOptions(ssl);
                if (isProtocolEnabled(opts, SSL.SSL_OP_NO_TLSv1_3, SslProtocols.TLS_v1_3)) {
                    extraCiphers = OpenSsl.EXTRA_SUPPORTED_TLS_1_3_CIPHERS;
                    tls13Enabled = true;
                } else {
                    extraCiphers = EMPTY_STRINGS;
                    tls13Enabled = false;
                }
            } else {
                return EMPTY_STRINGS;
            }
        }
        if (enabled == null) {
            return EMPTY_STRINGS;
        } else {
            Set<String> enabledSet = new LinkedHashSet<String>(enabled.length + extraCiphers.length);
            synchronized (this) {
                for (int i = 0; i < enabled.length; i++) {
                    String mapped = toJavaCipherSuite(enabled[i]);
                    final String cipher = mapped == null ? enabled[i] : mapped;
                    if ((!tls13Enabled || !OpenSsl.isTlsv13Supported()) && SslUtils.isTLSv13Cipher(cipher)) {
                        continue;
                    }
                    enabledSet.add(cipher);
                }
                Collections.addAll(enabledSet, extraCiphers);
            }
            return enabledSet.toArray(EMPTY_STRINGS);
        }
    }

    @Override
    public final void setEnabledCipherSuites(String[] cipherSuites) {
        checkNotNull(cipherSuites, "cipherSuites");

        final StringBuilder buf = new StringBuilder();
        final StringBuilder bufTLSv13 = new StringBuilder();

        CipherSuiteConverter.convertToCipherStrings(Arrays.asList(cipherSuites), buf, bufTLSv13, OpenSsl.isBoringSSL());
        final String cipherSuiteSpec = buf.toString();
        final String cipherSuiteSpecTLSv13 = bufTLSv13.toString();

        if (!OpenSsl.isTlsv13Supported() && !cipherSuiteSpecTLSv13.isEmpty()) {
            throw new IllegalArgumentException("TLSv1.3 is not supported by this java version.");
        }
        synchronized (this) {
            hasTLSv13Cipher = !cipherSuiteSpecTLSv13.isEmpty();
            if (!isDestroyed()) {
                try {
                    // Set non TLSv1.3 ciphers.
                    SSL.setCipherSuites(ssl, cipherSuiteSpec, false);
                    if (OpenSsl.isTlsv13Supported()) {
                        // Set TLSv1.3 ciphers.
                        SSL.setCipherSuites(ssl, OpenSsl.checkTls13Ciphers(logger, cipherSuiteSpecTLSv13), true);
                    }

                    // We also need to update the enabled protocols to ensure we disable the protocol if there are
                    // no compatible ciphers left.
                    Set<String> protocols = new HashSet<String>(enabledProtocols);

                    // We have no ciphers that are compatible with none-TLSv1.3, let us explicit disable all other
                    // protocols.
                    if (cipherSuiteSpec.isEmpty()) {
                        protocols.remove(SslProtocols.TLS_v1);
                        protocols.remove(SslProtocols.TLS_v1_1);
                        protocols.remove(SslProtocols.TLS_v1_2);
                        protocols.remove(SslProtocols.SSL_v3);
                        protocols.remove(SslProtocols.SSL_v2);
                        protocols.remove(SslProtocols.SSL_v2_HELLO);
                    }
                    // We have no ciphers that are compatible with TLSv1.3, let us explicit disable it.
                    if (cipherSuiteSpecTLSv13.isEmpty()) {
                        protocols.remove(SslProtocols.TLS_v1_3);
                    }
                    // Update the protocols but not cache the value. We only cache when we call it from the user
                    // code or when we construct the engine.
                    setEnabledProtocols0(protocols.toArray(EMPTY_STRINGS), !hasTLSv13Cipher);
                } catch (Exception e) {
                    throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec, e);
                }
            } else {
                throw new IllegalStateException("failed to enable cipher suites: " + cipherSuiteSpec);
            }
        }
    }

    @Override
    public final String[] getSupportedProtocols() {
        return OpenSsl.SUPPORTED_PROTOCOLS_SET.toArray(EMPTY_STRINGS);
    }

    @Override
    public final String[] getEnabledProtocols() {
        return enabledProtocols.toArray(EMPTY_STRINGS);
    }

    private static boolean isProtocolEnabled(int opts, int disableMask, String protocolString) {
        // We also need to check if the actual protocolString is supported as depending on the openssl API
        // implementations it may use a disableMask of 0 (BoringSSL is doing this for example).
        return (opts & disableMask) == 0 && OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(protocolString);
    }

    /**
     * {@inheritDoc}
     * TLS doesn't support a way to advertise non-contiguous versions from the client's perspective, and the client
     * just advertises the max supported version. The TLS protocol also doesn't support all different combinations of
     * discrete protocols, and instead assumes contiguous ranges. OpenSSL has some unexpected behavior
     * (e.g. handshake failures) if non-contiguous protocols are used even where there is a compatible set of protocols
     * and ciphers. For these reasons this method will determine the minimum protocol and the maximum protocol and
     * enabled a contiguous range from [min protocol, max protocol] in OpenSSL.
     */
    @Override
    public final void setEnabledProtocols(String[] protocols) {
        checkNotNullWithIAE(protocols, "protocols");
        synchronized (this) {
            enabledProtocols.clear();
            // Seems like there is no way to explicit disable SSLv2Hello in openssl, so it is always enabled
            enabledProtocols.add(SslProtocols.SSL_v2_HELLO);

            Collections.addAll(enabledProtocols, protocols);

            setEnabledProtocols0(protocols, !hasTLSv13Cipher);
        }
    }

    private void setEnabledProtocols0(String[] protocols, boolean explicitDisableTLSv13) {
        assert Thread.holdsLock(this);
        // This is correct from the API docs
        int minProtocolIndex = OPENSSL_OP_NO_PROTOCOLS.length;
        int maxProtocolIndex = 0;
        for (String p: protocols) {
            if (!OpenSsl.SUPPORTED_PROTOCOLS_SET.contains(p)) {
                throw new IllegalArgumentException("Protocol " + p + " is not supported.");
            }
            if (p.equals(SslProtocols.SSL_v2)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV2;
                }
            } else if (p.equals(SslProtocols.SSL_v3)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_SSLV3;
                }
            } else if (p.equals(SslProtocols.TLS_v1)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1;
                }
            } else if (p.equals(SslProtocols.TLS_v1_1)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_1;
                }
            } else if (p.equals(SslProtocols.TLS_v1_2)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_2;
                }
            } else if (!explicitDisableTLSv13 && p.equals(SslProtocols.TLS_v1_3)) {
                if (minProtocolIndex > OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3) {
                    minProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3;
                }
                if (maxProtocolIndex < OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3) {
                    maxProtocolIndex = OPENSSL_OP_NO_PROTOCOL_INDEX_TLSv1_3;
                }
            }
        }
        if (!isDestroyed()) {
            // Clear out options which disable protocols
            SSL.clearOptions(ssl, SSL.SSL_OP_NO_SSLv2 | SSL.SSL_OP_NO_SSLv3 | SSL.SSL_OP_NO_TLSv1 |
                    SSL.SSL_OP_NO_TLSv1_1 | SSL.SSL_OP_NO_TLSv1_2 | SSL.SSL_OP_NO_TLSv1_3);

            int opts = 0;
            for (int i = 0; i < minProtocolIndex; ++i) {
                opts |= OPENSSL_OP_NO_PROTOCOLS[i];
            }
            assert maxProtocolIndex != MAX_VALUE;
            for (int i = maxProtocolIndex + 1; i < OPENSSL_OP_NO_PROTOCOLS.length; ++i) {
                opts |= OPENSSL_OP_NO_PROTOCOLS[i];
            }

            // Disable protocols we do not want
            SSL.setOptions(ssl, opts);
        } else {
            throw new IllegalStateException("failed to enable protocols: " + Arrays.asList(protocols));
        }
    }

    @Override
    public final SSLSession getSession() {
        return session;
    }

    @Override
    public final synchronized void beginHandshake() throws SSLException {
        switch (handshakeState) {
            case STARTED_IMPLICITLY:
                checkEngineClosed();

                // A user did not start handshake by calling this method by him/herself,
                // but handshake has been started already by wrap() or unwrap() implicitly.
                // Because it's the user's first time to call this method, it is unfair to
                // raise an exception.  From the user's standpoint, he or she never asked
                // for renegotiation.

                handshakeState = HandshakeState.STARTED_EXPLICITLY; // Next time this method is invoked by the user,
                calculateMaxWrapOverhead();
                // we should raise an exception.
                break;
            case STARTED_EXPLICITLY:
                // Nothing to do as the handshake is not done yet.
                break;
            case FINISHED:
                throw new SSLException("renegotiation unsupported");
            case NOT_STARTED:
                handshakeState = HandshakeState.STARTED_EXPLICITLY;
                if (handshake() == NEED_TASK) {
                    // Set needTask to true so getHandshakeStatus() will return the correct value.
                    needTask = true;
                }
                calculateMaxWrapOverhead();
                break;
            default:
                throw new Error();
        }
    }

    private void checkEngineClosed() throws SSLException {
        if (isDestroyed()) {
            throw new SSLException("engine closed");
        }
    }

    private static SSLEngineResult.HandshakeStatus pendingStatus(int pendingStatus) {
        // Depending on if there is something left in the BIO we need to WRAP or UNWRAP
        return pendingStatus > 0 ? NEED_WRAP : NEED_UNWRAP;
    }

    private static boolean isEmpty(Object[] arr) {
        return arr == null || arr.length == 0;
    }

    private static boolean isEmpty(byte[] cert) {
        return cert == null || cert.length == 0;
    }

    private SSLEngineResult.HandshakeStatus handshakeException() throws SSLException {
        if (SSL.bioLengthNonApplication(networkBIO) > 0) {
            // There is something pending, we need to consume it first via a WRAP so we don't loose anything.
            return NEED_WRAP;
        }

        Throwable exception = pendingException;
        assert exception != null;
        pendingException = null;
        shutdown();
        if (exception instanceof SSLHandshakeException) {
            throw (SSLHandshakeException) exception;
        }
        SSLHandshakeException e = new SSLHandshakeException("General OpenSslEngine problem");
        e.initCause(exception);
        throw e;
    }

    /**
     * Should be called if the handshake will be failed due a callback that throws an exception.
     * This cause will then be used to give more details as part of the {@link SSLHandshakeException}.
     */
    final void initHandshakeException(Throwable cause) {
        if (pendingException == null) {
            pendingException = cause;
        } else {
            ThrowableUtil.addSuppressed(pendingException, cause);
        }
    }

    private SSLEngineResult.HandshakeStatus handshake() throws SSLException {
        if (needTask) {
            return NEED_TASK;
        }
        if (handshakeState == HandshakeState.FINISHED) {
            return FINISHED;
        }

        checkEngineClosed();

        if (pendingException != null) {
            // Let's call SSL.doHandshake(...) again in case there is some async operation pending that would fill the
            // outbound buffer.
            if (SSL.doHandshake(ssl) <= 0) {
                // Clear any error that was put on the stack by the handshake
                SSL.clearError();
            }
            return handshakeException();
        }

        // Adding the OpenSslEngine to the OpenSslEngineMap so it can be used in the AbstractCertificateVerifier.
        engineMap.add(this);

        if (!sessionSet) {
            if (!parentContext.sessionContext().setSessionFromCache(ssl, session, getPeerHost(), getPeerPort())) {
                // The session was not reused via the cache. Call prepareHandshake() to ensure we remove all previous
                // stored key-value pairs.
                session.prepareHandshake();
            }
            sessionSet = true;
        }

        int code = SSL.doHandshake(ssl);
        if (code <= 0) {
            int sslError = SSL.getError(ssl, code);
            if (sslError == SSL.SSL_ERROR_WANT_READ || sslError == SSL.SSL_ERROR_WANT_WRITE) {
                return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
            }

            if (sslError == SSL.SSL_ERROR_WANT_X509_LOOKUP ||
                    sslError == SSL.SSL_ERROR_WANT_CERTIFICATE_VERIFY ||
                    sslError == SSL.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION) {
                return NEED_TASK;
            }

            int errorNumber = SSL.getLastErrorNumber();
            if (needWrapAgain(errorNumber)) {
                // There is something that needs to be send to the remote peer before we can teardown.
                // This is most likely some alert.
                return NEED_WRAP;
            }
            // Check if we have a pending exception that was created during the handshake and if so throw it after
            // shutdown the connection.
            if (pendingException != null) {
                return handshakeException();
            }

            // Everything else is considered as error
            throw shutdownWithError("SSL_do_handshake", sslError, errorNumber);
        }
        // We have produced more data as part of the handshake if this is the case the user should call wrap(...)
        if (SSL.bioLengthNonApplication(networkBIO) > 0) {
            return NEED_WRAP;
        }
        // if SSL_do_handshake returns > 0 or sslError == SSL.SSL_ERROR_NAME it means the handshake was finished.
        session.handshakeFinished(SSL.getSessionId(ssl), SSL.getCipherForSSL(ssl), SSL.getVersion(ssl),
                SSL.getPeerCertificate(ssl), SSL.getPeerCertChain(ssl),
                SSL.getTime(ssl) * 1000L, parentContext.sessionTimeout() * 1000L);
        selectApplicationProtocol();
        return FINISHED;
    }

    private SSLEngineResult.HandshakeStatus mayFinishHandshake(
            SSLEngineResult.HandshakeStatus hs, int bytesConsumed, int bytesProduced) throws SSLException {
        return hs == NEED_UNWRAP && bytesProduced > 0 || hs == NEED_WRAP && bytesConsumed > 0 ?
            handshake() : mayFinishHandshake(hs != FINISHED ? getHandshakeStatus() : FINISHED);
    }

    private SSLEngineResult.HandshakeStatus mayFinishHandshake(SSLEngineResult.HandshakeStatus status)
            throws SSLException {
        if (status == NOT_HANDSHAKING) {
            if (handshakeState != HandshakeState.FINISHED) {
                // If the status was NOT_HANDSHAKING and we not finished the handshake we need to call
                // SSL_do_handshake() again
                return handshake();
            }
            if (!isDestroyed() && SSL.bioLengthNonApplication(networkBIO) > 0) {
                // We have something left that needs to be wrapped.
                return NEED_WRAP;
            }
        }
        return status;
    }

    @Override
    public final synchronized SSLEngineResult.HandshakeStatus getHandshakeStatus() {
        // Check if we are in the initial handshake phase or shutdown phase
        if (needPendingStatus()) {
            if (needTask) {
                // There is a task outstanding
                return NEED_TASK;
            }
            return pendingStatus(SSL.bioLengthNonApplication(networkBIO));
        }
        return NOT_HANDSHAKING;
    }

    private SSLEngineResult.HandshakeStatus getHandshakeStatus(int pending) {
        // Check if we are in the initial handshake phase or shutdown phase
        if (needPendingStatus()) {
            if (needTask) {
                // There is a task outstanding
                return NEED_TASK;
            }
            return pendingStatus(pending);
        }
        return NOT_HANDSHAKING;
    }

    private boolean needPendingStatus() {
        return handshakeState != HandshakeState.NOT_STARTED && !isDestroyed()
                && (handshakeState != HandshakeState.FINISHED || isInboundDone() || isOutboundDone());
    }

    /**
     * Converts the specified OpenSSL cipher suite to the Java cipher suite.
     */
    private String toJavaCipherSuite(String openSslCipherSuite) {
        if (openSslCipherSuite == null) {
            return null;
        }

        String version = SSL.getVersion(ssl);
        String prefix = toJavaCipherSuitePrefix(version);
        return CipherSuiteConverter.toJava(openSslCipherSuite, prefix);
    }

    /**
     * Converts the protocol version string returned by {@link SSL#getVersion(long)} to protocol family string.
     */
    private static String toJavaCipherSuitePrefix(String protocolVersion) {
        final char c;
        if (protocolVersion == null || protocolVersion.isEmpty()) {
            c = 0;
        } else {
            c = protocolVersion.charAt(0);
        }

        switch (c) {
            case 'T':
                return "TLS";
            case 'S':
                return "SSL";
            default:
                return "UNKNOWN";
        }
    }

    @Override
    public final void setUseClientMode(boolean clientMode) {
        if (clientMode != this.clientMode) {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public final boolean getUseClientMode() {
        return clientMode;
    }

    @Override
    public final void setNeedClientAuth(boolean b) {
        setClientAuth(b ? ClientAuth.REQUIRE : ClientAuth.NONE);
    }

    @Override
    public final boolean getNeedClientAuth() {
        return clientAuth == ClientAuth.REQUIRE;
    }

    @Override
    public final void setWantClientAuth(boolean b) {
        setClientAuth(b ? ClientAuth.OPTIONAL : ClientAuth.NONE);
    }

    @Override
    public final boolean getWantClientAuth() {
        return clientAuth == ClientAuth.OPTIONAL;
    }

    /**
     * See <a href="https://www.openssl.org/docs/man1.0.2/ssl/SSL_set_verify.html">SSL_set_verify</a> and
     * {@link SSL#setVerify(long, int, int)}.
     */
    @UnstableApi
    public final synchronized void setVerify(int verifyMode, int depth) {
        if (!isDestroyed()) {
            SSL.setVerify(ssl, verifyMode, depth);
        }
    }

    private void setClientAuth(ClientAuth mode) {
        if (clientMode) {
            return;
        }
        synchronized (this) {
            if (clientAuth == mode) {
                // No need to issue any JNI calls if the mode is the same
                return;
            }
            if (!isDestroyed()) {
                switch (mode) {
                    case NONE:
                        SSL.setVerify(ssl, SSL.SSL_CVERIFY_NONE, ReferenceCountedOpenSslContext.VERIFY_DEPTH);
                        break;
                    case REQUIRE:
                        SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRED, ReferenceCountedOpenSslContext.VERIFY_DEPTH);
                        break;
                    case OPTIONAL:
                        SSL.setVerify(ssl, SSL.SSL_CVERIFY_OPTIONAL, ReferenceCountedOpenSslContext.VERIFY_DEPTH);
                        break;
                    default:
                        throw new Error(mode.toString());
                }
            }
            clientAuth = mode;
        }
    }

    @Override
    public final void setEnableSessionCreation(boolean b) {
        if (b) {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public final boolean getEnableSessionCreation() {
        return false;
    }

    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
    @Override
    public final synchronized SSLParameters getSSLParameters() {
        SSLParameters sslParameters = super.getSSLParameters();

        int version = PlatformDependent.javaVersion();
        if (version >= 7) {
            Java7SslParametersUtils.setEndpointIdentificationAlgorithm(sslParameters, endpointIdentificationAlgorithm);
            Java7SslParametersUtils.setAlgorithmConstraints(sslParameters, algorithmConstraints);
            if (version >= 8) {
                if (sniHostNames != null) {
                    Java8SslUtils.setSniHostNames(sslParameters, sniHostNames);
                }
                if (!isDestroyed()) {
                    Java8SslUtils.setUseCipherSuitesOrder(
                            sslParameters, (SSL.getOptions(ssl) & SSL.SSL_OP_CIPHER_SERVER_PREFERENCE) != 0);
                }

                Java8SslUtils.setSNIMatchers(sslParameters, matchers);
            }
        }
        return sslParameters;
    }

    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
    @Override
    public final synchronized void setSSLParameters(SSLParameters sslParameters) {
        int version = PlatformDependent.javaVersion();
        if (version >= 7) {
            if (sslParameters.getAlgorithmConstraints() != null) {
                throw new IllegalArgumentException("AlgorithmConstraints are not supported.");
            }

            boolean isDestroyed = isDestroyed();
            if (version >= 8) {
                if (!isDestroyed) {
                    if (clientMode) {
                        final List<String> sniHostNames = Java8SslUtils.getSniHostNames(sslParameters);
                        for (String name: sniHostNames) {
                            SSL.setTlsExtHostName(ssl, name);
                        }
                        this.sniHostNames = sniHostNames;
                    }
                    if (Java8SslUtils.getUseCipherSuitesOrder(sslParameters)) {
                        SSL.setOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE);
                    } else {
                        SSL.clearOptions(ssl, SSL.SSL_OP_CIPHER_SERVER_PREFERENCE);
                    }
                }
                matchers = sslParameters.getSNIMatchers();
            }

            final String endpointIdentificationAlgorithm = sslParameters.getEndpointIdentificationAlgorithm();
            if (!isDestroyed) {
                configureEndpointVerification(endpointIdentificationAlgorithm);
            }
            this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;
            algorithmConstraints = sslParameters.getAlgorithmConstraints();
        }
        super.setSSLParameters(sslParameters);
    }

    private void configureEndpointVerification(String endpointIdentificationAlgorithm) {
        // If the user asks for hostname verification we must ensure we verify the peer.
        // If the user disables hostname verification we leave it up to the user to change the mode manually.
        if (clientMode && isEndPointVerificationEnabled(endpointIdentificationAlgorithm)) {
            SSL.setVerify(ssl, SSL.SSL_CVERIFY_REQUIRED, -1);
        }
    }

    private static boolean isEndPointVerificationEnabled(String endPointIdentificationAlgorithm) {
        return endPointIdentificationAlgorithm != null && !endPointIdentificationAlgorithm.isEmpty();
    }

    private boolean isDestroyed() {
        return destroyed;
    }

    final boolean checkSniHostnameMatch(byte[] hostname) {
        return Java8SslUtils.checkSniHostnameMatch(matchers, hostname);
    }

    @Override
    public String getNegotiatedApplicationProtocol() {
        return applicationProtocol;
    }

    private static long bufferAddress(ByteBuffer b) {
        assert b.isDirect();
        if (PlatformDependent.hasUnsafe()) {
            return PlatformDependent.directBufferAddress(b);
        }
        return Buffer.address(b);
    }

    /**
     * Select the application protocol used.
     */
    private void selectApplicationProtocol() throws SSLException {
        ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior = apn.selectedListenerFailureBehavior();
        List<String> protocols = apn.protocols();
        String applicationProtocol;
        switch (apn.protocol()) {
            case NONE:
                break;
            // We always need to check for applicationProtocol == null as the remote peer may not support
            // the TLS extension or may have returned an empty selection.
            case ALPN:
                applicationProtocol = SSL.getAlpnSelected(ssl);
                if (applicationProtocol != null) {
                    ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol(
                            protocols, behavior, applicationProtocol);
                }
                break;
            case NPN:
                applicationProtocol = SSL.getNextProtoNegotiated(ssl);
                if (applicationProtocol != null) {
                    ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol(
                            protocols, behavior, applicationProtocol);
                }
                break;
            case NPN_AND_ALPN:
                applicationProtocol = SSL.getAlpnSelected(ssl);
                if (applicationProtocol == null) {
                    applicationProtocol = SSL.getNextProtoNegotiated(ssl);
                }
                if (applicationProtocol != null) {
                    ReferenceCountedOpenSslEngine.this.applicationProtocol = selectApplicationProtocol(
                            protocols, behavior, applicationProtocol);
                }
                break;
            default:
                throw new Error();
        }
    }

    private String selectApplicationProtocol(List<String> protocols,
                                             ApplicationProtocolConfig.SelectedListenerFailureBehavior behavior,
                                             String applicationProtocol) throws SSLException {
        if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT) {
            return applicationProtocol;
        } else {
            int size = protocols.size();
            assert size > 0;
            if (protocols.contains(applicationProtocol)) {
                return applicationProtocol;
            } else {
                if (behavior == ApplicationProtocolConfig.SelectedListenerFailureBehavior.CHOOSE_MY_LAST_PROTOCOL) {
                    return protocols.get(size - 1);
                } else {
                    throw new SSLException("unknown protocol " + applicationProtocol);
                }
            }
        }
    }

    private static final X509Certificate[] JAVAX_CERTS_NOT_SUPPORTED = new X509Certificate[0];

    private final class DefaultOpenSslSession implements OpenSslInternalSession {
        private final OpenSslSessionContext sessionContext;

        // These are guarded by synchronized(OpenSslEngine.this) as handshakeFinished() may be triggered by any
        // thread.
        private X509Certificate[] x509PeerCerts;
        private Certificate[] peerCerts;

        private boolean valid = true;
        private String protocol;
        private String cipher;
        private OpenSslSessionId id = OpenSslSessionId.NULL_ID;
        private long creationTime;

        // Updated once a new handshake is started and so the SSLSession reused.
        private long lastAccessed = -1;

        private volatile int applicationBufferSize = MAX_PLAINTEXT_LENGTH;
        private volatile Certificate[] localCertificateChain;
        private volatile Map<String, Object> keyValueStorage = new ConcurrentHashMap<String, Object>();

        DefaultOpenSslSession(OpenSslSessionContext sessionContext) {
            this.sessionContext = sessionContext;
        }

        private SSLSessionBindingEvent newSSLSessionBindingEvent(String name) {
            return new SSLSessionBindingEvent(session, name);
        }

        @Override
        public void prepareHandshake() {
            keyValueStorage.clear();
        }

        @Override
        public void setSessionDetails(
                long creationTime, long lastAccessedTime, OpenSslSessionId sessionId,
                Map<String, Object> keyValueStorage) {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (this.id == OpenSslSessionId.NULL_ID) {
                    this.id = sessionId;
                    this.creationTime = creationTime;
                    this.lastAccessed = lastAccessedTime;

                    // Update the key value storage. It's fine to just drop the previous stored values on the floor
                    // as the JDK does the same in the sense that it will use a new SSLSessionImpl instance once the
                    // handshake was done
                    this.keyValueStorage = keyValueStorage;
                }
            }
        }

        @Override
        public Map<String, Object> keyValueStorage() {
            return keyValueStorage;
        }

        @Override
        public OpenSslSessionId sessionId() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (this.id == OpenSslSessionId.NULL_ID && !isDestroyed()) {
                    byte[] sessionId = SSL.getSessionId(ssl);
                    if (sessionId != null) {
                        id = new OpenSslSessionId(sessionId);
                    }
                }

                return id;
            }
        }

        @Override
        public void setLocalCertificate(Certificate[] localCertificate) {
            this.localCertificateChain = localCertificate;
        }

        @Override
        public byte[] getId() {
            return sessionId().cloneBytes();
        }

        @Override
        public OpenSslSessionContext getSessionContext() {
            return sessionContext;
        }

        @Override
        public long getCreationTime() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                return creationTime;
            }
        }

        @Override
        public void setLastAccessedTime(long time) {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                this.lastAccessed = time;
            }
        }

        @Override
        public long getLastAccessedTime() {
            // if lastAccessed is -1 we will just return the creation time as the handshake was not started yet.
            synchronized (ReferenceCountedOpenSslEngine.this) {
                return lastAccessed == -1 ? creationTime : lastAccessed;
            }
        }

        @Override
        public void invalidate() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                valid = false;
                sessionContext.removeFromCache(id);
            }
        }

        @Override
        public boolean isValid() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                return valid || sessionContext.isInCache(id);
            }
        }

        @Override
        public void putValue(String name, Object value) {
            checkNotNull(name, "name");
            checkNotNull(value, "value");

            final Object old = keyValueStorage.put(name, value);
            if (value instanceof SSLSessionBindingListener) {
                // Use newSSLSessionBindingEvent so we always use the wrapper if needed.
                ((SSLSessionBindingListener) value).valueBound(newSSLSessionBindingEvent(name));
            }
            notifyUnbound(old, name);
        }

        @Override
        public Object getValue(String name) {
            checkNotNull(name, "name");
            return keyValueStorage.get(name);
        }

        @Override
        public void removeValue(String name) {
            checkNotNull(name, "name");
            final Object old = keyValueStorage.remove(name);
            notifyUnbound(old, name);
        }

        @Override
        public String[] getValueNames() {
            return keyValueStorage.keySet().toArray(EMPTY_STRINGS);
        }

        private void notifyUnbound(Object value, String name) {
            if (value instanceof SSLSessionBindingListener) {
                // Use newSSLSessionBindingEvent so we always use the wrapper if needed.
                ((SSLSessionBindingListener) value).valueUnbound(newSSLSessionBindingEvent(name));
            }
        }

        /**
         * Finish the handshake and so init everything in the {@link OpenSslInternalSession} that should be accessible
         * by the user.
         */
        @Override
        public void handshakeFinished(byte[] id, String cipher, String protocol, byte[] peerCertificate,
                                      byte[][] peerCertificateChain, long creationTime, long timeout)
                throws SSLException {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (!isDestroyed()) {
                    if (this.id == OpenSslSessionId.NULL_ID) {
                        // if the handshake finished and it was not a resumption let ensure we try to set the id

                        this.id = id == null ? OpenSslSessionId.NULL_ID : new OpenSslSessionId(id);
                        // Once the handshake was done the lastAccessed and creationTime should be the same if we
                        // did not set it earlier via setSessionDetails(...)
                        this.creationTime = lastAccessed = creationTime;
                    }
                    this.cipher = toJavaCipherSuite(cipher);
                    this.protocol = protocol;

                    if (clientMode) {
                        if (isEmpty(peerCertificateChain)) {
                            peerCerts = EmptyArrays.EMPTY_CERTIFICATES;
                            if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) {
                                x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
                            } else {
                                x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED;
                            }
                        } else {
                            peerCerts = new Certificate[peerCertificateChain.length];
                            if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) {
                                x509PeerCerts = new X509Certificate[peerCertificateChain.length];
                            } else {
                                x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED;
                            }
                            initCerts(peerCertificateChain, 0);
                        }
                    } else {
                        // if used on the server side SSL_get_peer_cert_chain(...) will not include the remote peer
                        // certificate. We use SSL_get_peer_certificate to get it in this case and add it to our
                        // array later.
                        //
                        // See https://www.openssl.org/docs/ssl/SSL_get_peer_cert_chain.html
                        if (isEmpty(peerCertificate)) {
                            peerCerts = EmptyArrays.EMPTY_CERTIFICATES;
                            x509PeerCerts = EmptyArrays.EMPTY_JAVAX_X509_CERTIFICATES;
                        } else {
                            if (isEmpty(peerCertificateChain)) {
                                peerCerts = new Certificate[] {new LazyX509Certificate(peerCertificate)};
                                if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) {
                                    x509PeerCerts = new X509Certificate[] {
                                            new LazyJavaxX509Certificate(peerCertificate)
                                    };
                                } else {
                                    x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED;
                                }
                            } else {
                                peerCerts = new Certificate[peerCertificateChain.length + 1];
                                peerCerts[0] = new LazyX509Certificate(peerCertificate);

                                if (OpenSsl.JAVAX_CERTIFICATE_CREATION_SUPPORTED) {
                                    x509PeerCerts = new X509Certificate[peerCertificateChain.length + 1];
                                    x509PeerCerts[0] = new LazyJavaxX509Certificate(peerCertificate);
                                } else {
                                    x509PeerCerts = JAVAX_CERTS_NOT_SUPPORTED;
                                }

                                initCerts(peerCertificateChain, 1);
                            }
                        }
                    }

                    calculateMaxWrapOverhead();

                    handshakeState = HandshakeState.FINISHED;
                } else {
                    throw new SSLException("Already closed");
                }
            }
        }

        private void initCerts(byte[][] chain, int startPos) {
            for (int i = 0; i < chain.length; i++) {
                int certPos = startPos + i;
                peerCerts[certPos] = new LazyX509Certificate(chain[i]);
                if (x509PeerCerts != JAVAX_CERTS_NOT_SUPPORTED) {
                    x509PeerCerts[certPos] = new LazyJavaxX509Certificate(chain[i]);
                }
            }
        }

        @Override
        public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (isEmpty(peerCerts)) {
                    throw new SSLPeerUnverifiedException("peer not verified");
                }
                return peerCerts.clone();
            }
        }

        @Override
        public boolean hasPeerCertificates() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                return !isEmpty(peerCerts);
            }
        }

        @Override
        public Certificate[] getLocalCertificates() {
            Certificate[] localCerts = this.localCertificateChain;
            if (localCerts == null) {
                return null;
            }
            return localCerts.clone();
        }

        @Override
        public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (x509PeerCerts == JAVAX_CERTS_NOT_SUPPORTED) {
                    // Not supported by the underlying JDK, so just throw. This is fine in terms of the API
                    // contract. See SSLSession.html#getPeerCertificateChain().
                    throw new UnsupportedOperationException();
                }
                if (isEmpty(x509PeerCerts)) {
                    throw new SSLPeerUnverifiedException("peer not verified");
                }
                return x509PeerCerts.clone();
            }
        }

        @Override
        public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
            Certificate[] peer = getPeerCertificates();
            // No need for null or length > 0 is needed as this is done in getPeerCertificates()
            // already.
            return ((java.security.cert.X509Certificate) peer[0]).getSubjectX500Principal();
        }

        @Override
        public Principal getLocalPrincipal() {
            Certificate[] local = this.localCertificateChain;
            if (local == null || local.length == 0) {
                return null;
            }
            return ((java.security.cert.X509Certificate) local[0]).getSubjectX500Principal();
        }

        @Override
        public String getCipherSuite() {
            synchronized (ReferenceCountedOpenSslEngine.this) {
                if (cipher == null) {
                    return SslUtils.INVALID_CIPHER;
                }
                return cipher;
            }
        }

        @Override
        public String getProtocol() {
            String protocol = this.protocol;
            if (protocol == null) {
                synchronized (ReferenceCountedOpenSslEngine.this) {
                    if (!isDestroyed()) {
                        protocol = SSL.getVersion(ssl);
                    } else {
                        protocol = StringUtil.EMPTY_STRING;
                    }
                }
            }
            return protocol;
        }

        @Override
        public String getPeerHost() {
            return ReferenceCountedOpenSslEngine.this.getPeerHost();
        }

        @Override
        public int getPeerPort() {
            return ReferenceCountedOpenSslEngine.this.getPeerPort();
        }

        @Override
        public int getPacketBufferSize() {
            return SSL.SSL_MAX_ENCRYPTED_LENGTH;
        }

        @Override
        public int getApplicationBufferSize() {
            return applicationBufferSize;
        }

        @Override
        public void tryExpandApplicationBufferSize(int packetLengthDataOnly) {
            if (packetLengthDataOnly > MAX_PLAINTEXT_LENGTH && applicationBufferSize != MAX_RECORD_SIZE) {
                applicationBufferSize = MAX_RECORD_SIZE;
            }
        }

        @Override
        public String toString() {
            return "DefaultOpenSslSession{" +
                    "sessionContext=" + sessionContext +
                    ", id=" + id +
                    '}';
        }

        @Override
        public int hashCode() {
            return sessionId().hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) {
                return true;
            }
            // We trust all sub-types as we use different types but the interface is package-private
            if (!(o instanceof OpenSslInternalSession)) {
                return false;
            }
            return sessionId().equals(((OpenSslInternalSession) o).sessionId());
        }
    }

    private interface NativeSslException {
        int errorCode();
    }

    private static final class OpenSslException extends SSLException implements NativeSslException {
        private final int errorCode;

        OpenSslException(String reason, int errorCode) {
            super(reason);
            this.errorCode = errorCode;
        }

        @Override
        public int errorCode() {
            return errorCode;
        }
    }

    private static final class OpenSslHandshakeException extends SSLHandshakeException implements NativeSslException {
        private final int errorCode;

        OpenSslHandshakeException(String reason, int errorCode) {
            super(reason);
            this.errorCode = errorCode;
        }

        @Override
        public int errorCode() {
            return errorCode;
        }
    }
}
