//
//  ========================================================================
//  Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util.ssl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CRL;
import java.security.cert.CertStore;
import java.security.cert.CertStoreParameters;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.PKIXCertPathChecker;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIMatcher;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.StandardConstants;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509TrustManager;

import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.CertificateUtils;
import org.eclipse.jetty.util.security.CertificateValidator;
import org.eclipse.jetty.util.security.Password;

/**
 * <p>SslContextFactory is used to configure SSL parameters
 * to be used by server and client connectors.</p>
 * <p>Use {@link Server} to configure server-side connectors,
 * and {@link Client} to configure HTTP or WebSocket clients.</p>
 */
@ManagedObject
public class SslContextFactory extends AbstractLifeCycle implements Dumpable
{
    public static final TrustManager[] TRUST_ALL_CERTS = new X509TrustManager[]{
        new X509ExtendedTrustManager()
        {
            @Override
            public X509Certificate[] getAcceptedIssuers()
            {
                return new X509Certificate[0];
            }

            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType)
            {
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
            {
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
            {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType)
            {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
            {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
            {
            }
        }
    };

    private static final Logger LOG = Log.getLogger(SslContextFactory.class);
    private static final Logger LOG_CONFIG = LOG.getLogger("config");

    public static final String DEFAULT_KEYMANAGERFACTORY_ALGORITHM = KeyManagerFactory.getDefaultAlgorithm();

    public static final String DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM = TrustManagerFactory.getDefaultAlgorithm();

    /**
     * String name of key password property.
     */
    public static final String KEYPASSWORD_PROPERTY = "org.eclipse.jetty.ssl.keypassword";

    /**
     * String name of keystore password property.
     */
    public static final String PASSWORD_PROPERTY = "org.eclipse.jetty.ssl.password";

    /**
     * Default Excluded Protocols List
     */
    private static final String[] DEFAULT_EXCLUDED_PROTOCOLS = {"SSL", "SSLv2", "SSLv2Hello", "SSLv3"};

    /**
     * Default Excluded Cipher Suite List
     */
    private static final String[] DEFAULT_EXCLUDED_CIPHER_SUITES = {
        // Exclude weak / insecure ciphers
        "^.*_(MD5|SHA|SHA1)$",
        // Exclude ciphers that don't support forward secrecy
        "^TLS_RSA_.*$",
        // The following exclusions are present to cleanup known bad cipher
        // suites that may be accidentally included via include patterns.
        // The default enabled cipher list in Java will not include these
        // (but they are available in the supported list).
        "^SSL_.*$",
        "^.*_NULL_.*$",
        "^.*_anon_.*$"
    };

    private final Set<String> _excludeProtocols = new LinkedHashSet<>();
    private final Set<String> _includeProtocols = new LinkedHashSet<>();
    private final Set<String> _excludeCipherSuites = new LinkedHashSet<>();
    private final List<String> _includeCipherSuites = new ArrayList<>();
    private final Map<String, X509> _aliasX509 = new HashMap<>();
    private final Map<String, X509> _certHosts = new HashMap<>();
    private final Map<String, X509> _certWilds = new HashMap<>();
    private String[] _selectedProtocols;
    private boolean _useCipherSuitesOrder = true;
    private Comparator<String> _cipherComparator;
    private String[] _selectedCipherSuites;
    private Resource _keyStoreResource;
    private String _keyStoreProvider;
    private String _keyStoreType = "JKS";
    private String _certAlias;
    private Resource _trustStoreResource;
    private String _trustStoreProvider;
    private String _trustStoreType;
    private boolean _needClientAuth = false;
    private boolean _wantClientAuth = false;
    private Password _keyStorePassword;
    private Password _keyManagerPassword;
    private Password _trustStorePassword;
    private String _sslProvider;
    private String _sslProtocol = "TLS";
    private String _secureRandomAlgorithm;
    private String _keyManagerFactoryAlgorithm = DEFAULT_KEYMANAGERFACTORY_ALGORITHM;
    private String _trustManagerFactoryAlgorithm = DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM;
    private boolean _validateCerts;
    private boolean _validatePeerCerts;
    private int _maxCertPathLength = -1;
    private String _crlPath;
    private boolean _enableCRLDP = false;
    private boolean _enableOCSP = false;
    private String _ocspResponderURL;
    private KeyStore _setKeyStore;
    private KeyStore _setTrustStore;
    private boolean _sessionCachingEnabled = true;
    private int _sslSessionCacheSize = -1;
    private int _sslSessionTimeout = -1;
    private SSLContext _setContext;
    private String _endpointIdentificationAlgorithm = "HTTPS";
    private boolean _trustAll;
    private boolean _renegotiationAllowed = true;
    private int _renegotiationLimit = 5;
    private Factory _factory;
    private PKIXCertPathChecker _pkixCertPathChecker;
    private HostnameVerifier _hostnameVerifier;

    /**
     * Construct an instance of SslContextFactory with the default configuration.
     *
     * @deprecated use {@link Client#Client()} or {@link Server#Server()} instead
     */
    @Deprecated
    public SslContextFactory()
    {
        this(false);
    }

    /**
     * Construct an instance of SslContextFactory
     * Default constructor for use in XmlConfiguration files
     *
     * @param trustAll whether to blindly trust all certificates
     * @see #setTrustAll(boolean)
     * @deprecated use {@link Client#Client(boolean)} instead
     */
    @Deprecated
    public SslContextFactory(boolean trustAll)
    {
        this(trustAll, null);
    }

    /**
     * Construct an instance of SslContextFactory
     *
     * @param keyStorePath default keystore location
     * @deprecated use {@link #setKeyStorePath(String)} instead
     */
    @Deprecated
    public SslContextFactory(String keyStorePath)
    {
        this(false, keyStorePath);
    }

    private SslContextFactory(boolean trustAll, String keyStorePath)
    {
        setTrustAll(trustAll);
        setExcludeProtocols(DEFAULT_EXCLUDED_PROTOCOLS);
        setExcludeCipherSuites(DEFAULT_EXCLUDED_CIPHER_SUITES);

        if (keyStorePath != null)
            setKeyStorePath(keyStorePath);
    }

    /**
     * Creates the SSLContext object and starts the lifecycle
     */
    @Override
    protected void doStart() throws Exception
    {
        super.doStart();
        synchronized (this)
        {
            load();
        }
        checkConfiguration();
    }

    protected void checkConfiguration()
    {
        SSLEngine engine = _factory._context.createSSLEngine();
        customize(engine);
        SSLParameters supported = engine.getSSLParameters();

        checkProtocols(supported);
        checkCiphers(supported);
    }

    protected void checkTrustAll()
    {
        if (isTrustAll())
            LOG_CONFIG.warn("Trusting all certificates configured for {}", this);
    }

    protected void checkEndPointIdentificationAlgorithm()
    {
        if (getEndpointIdentificationAlgorithm() == null)
            LOG_CONFIG.warn("No Client EndPointIdentificationAlgorithm configured for {}", this);
    }

    protected void checkProtocols(SSLParameters supported)
    {
        for (String protocol : supported.getProtocols())
        {
            for (String excluded : DEFAULT_EXCLUDED_PROTOCOLS)
            {
                if (excluded.equals(protocol))
                    LOG_CONFIG.warn("Protocol {} not excluded for {}", protocol, this);
            }
        }
    }

    protected void checkCiphers(SSLParameters supported)
    {
        for (String suite : supported.getCipherSuites())
        {
            for (String excludedSuiteRegex : DEFAULT_EXCLUDED_CIPHER_SUITES)
            {
                if (suite.matches(excludedSuiteRegex))
                    LOG_CONFIG.warn("Weak cipher suite {} enabled for {}", suite, this);
            }
        }
    }

    private void load() throws Exception
    {
        SSLContext context = _setContext;
        KeyStore keyStore = _setKeyStore;
        KeyStore trustStore = _setTrustStore;

        if (context == null)
        {
            // Is this an empty factory?
            if (keyStore == null && _keyStoreResource == null && trustStore == null && _trustStoreResource == null)
            {
                TrustManager[] trustManagers = null;

                if (isTrustAll())
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("No keystore or trust store configured.  ACCEPTING UNTRUSTED CERTIFICATES!!!!!");
                    // Create a trust manager that does not validate certificate chains
                    trustManagers = TRUST_ALL_CERTS;
                }

                context = getSSLContextInstance();
                context.init(null, trustManagers, getSecureRandomInstance());
            }
            else
            {
                if (keyStore == null)
                    keyStore = loadKeyStore(_keyStoreResource);
                if (trustStore == null)
                    trustStore = loadTrustStore(_trustStoreResource);

                Collection<? extends CRL> crls = loadCRL(getCrlPath());

                // Look for X.509 certificates to create alias map
                if (keyStore != null)
                {
                    for (String alias : Collections.list(keyStore.aliases()))
                    {
                        Certificate certificate = keyStore.getCertificate(alias);
                        if (certificate != null && "X.509".equals(certificate.getType()))
                        {
                            X509Certificate x509C = (X509Certificate)certificate;

                            // Exclude certificates with special uses
                            if (X509.isCertSign(x509C))
                            {
                                if (LOG.isDebugEnabled())
                                    LOG.debug("Skipping " + x509C);
                                continue;
                            }
                            X509 x509 = new X509(alias, x509C);
                            _aliasX509.put(alias, x509);

                            if (isValidateCerts())
                            {
                                CertificateValidator validator = new CertificateValidator(trustStore, crls);
                                validator.setMaxCertPathLength(getMaxCertPathLength());
                                validator.setEnableCRLDP(isEnableCRLDP());
                                validator.setEnableOCSP(isEnableOCSP());
                                validator.setOcspResponderURL(getOcspResponderURL());
                                validator.validate(keyStore, x509C); // TODO what about truststore?
                            }

                            LOG.info("x509={} for {}", x509, this);

                            for (String h : x509.getHosts())
                            {
                                _certHosts.put(h, x509);
                            }
                            for (String w : x509.getWilds())
                            {
                                _certWilds.put(w, x509);
                            }
                        }
                    }
                }

                // Instantiate key and trust managers
                KeyManager[] keyManagers = getKeyManagers(keyStore);
                TrustManager[] trustManagers = getTrustManagers(trustStore, crls);

                // Initialize context
                context = getSSLContextInstance();
                context.init(keyManagers, trustManagers, getSecureRandomInstance());
            }
        }

        // Initialize cache
        SSLSessionContext serverContext = context.getServerSessionContext();
        if (serverContext != null)
        {
            if (getSslSessionCacheSize() > -1)
                serverContext.setSessionCacheSize(getSslSessionCacheSize());
            if (getSslSessionTimeout() > -1)
                serverContext.setSessionTimeout(getSslSessionTimeout());
        }

        // select the protocols and ciphers
        SSLParameters enabled = context.getDefaultSSLParameters();
        SSLParameters supported = context.getSupportedSSLParameters();
        selectCipherSuites(enabled.getCipherSuites(), supported.getCipherSuites());
        selectProtocols(enabled.getProtocols(), supported.getProtocols());

        _factory = new Factory(keyStore, trustStore, context);
        if (LOG.isDebugEnabled())
        {
            LOG.debug("Selected Protocols {} of {}", Arrays.asList(_selectedProtocols), Arrays.asList(supported.getProtocols()));
            LOG.debug("Selected Ciphers   {} of {}", Arrays.asList(_selectedCipherSuites), Arrays.asList(supported.getCipherSuites()));
        }
    }

    @Override
    public String dump()
    {
        return Dumpable.dump(this);
    }

    @Override
    public void dump(Appendable out, String indent) throws IOException
    {
        try
        {
            SSLEngine sslEngine = SSLContext.getDefault().createSSLEngine();
            Dumpable.dumpObjects(out, indent, this, "trustAll=" + _trustAll,
                new SslSelectionDump("Protocol",
                    sslEngine.getSupportedProtocols(),
                    sslEngine.getEnabledProtocols(),
                    getExcludeProtocols(),
                    getIncludeProtocols()),
                new SslSelectionDump("Cipher Suite",
                    sslEngine.getSupportedCipherSuites(),
                    sslEngine.getEnabledCipherSuites(),
                    getExcludeCipherSuites(),
                    getIncludeCipherSuites()));
        }
        catch (NoSuchAlgorithmException x)
        {
            LOG.ignore(x);
        }
    }

    List<SslSelectionDump> selectionDump() throws NoSuchAlgorithmException
    {
        /* Use a pristine SSLEngine (not one from this SslContextFactory).
         * This will allow for proper detection and identification
         * of JRE/lib/security/java.security level disabled features
         */
        SSLEngine sslEngine = SSLContext.getDefault().createSSLEngine();

        List<SslSelectionDump> selections = new ArrayList<>();

        // protocols
        selections.add(new SslSelectionDump("Protocol",
            sslEngine.getSupportedProtocols(),
            sslEngine.getEnabledProtocols(),
            getExcludeProtocols(),
            getIncludeProtocols()));

        // ciphers
        selections.add(new SslSelectionDump("Cipher Suite",
            sslEngine.getSupportedCipherSuites(),
            sslEngine.getEnabledCipherSuites(),
            getExcludeCipherSuites(),
            getIncludeCipherSuites()));

        return selections;
    }

    @Override
    protected void doStop() throws Exception
    {
        synchronized (this)
        {
            unload();
        }
        super.doStop();
    }

    private void unload()
    {
        _factory = null;
        _selectedProtocols = null;
        _selectedCipherSuites = null;
        _aliasX509.clear();
        _certHosts.clear();
        _certWilds.clear();
    }

    @ManagedAttribute(value = "The selected TLS protocol versions", readonly = true)
    public String[] getSelectedProtocols()
    {
        return Arrays.copyOf(_selectedProtocols, _selectedProtocols.length);
    }

    @ManagedAttribute(value = "The selected cipher suites", readonly = true)
    public String[] getSelectedCipherSuites()
    {
        return Arrays.copyOf(_selectedCipherSuites, _selectedCipherSuites.length);
    }

    public Comparator<String> getCipherComparator()
    {
        return _cipherComparator;
    }

    public void setCipherComparator(Comparator<String> cipherComparator)
    {
        if (cipherComparator != null)
            setUseCipherSuitesOrder(true);
        _cipherComparator = cipherComparator;
    }

    public Set<String> getAliases()
    {
        return Collections.unmodifiableSet(_aliasX509.keySet());
    }

    public X509 getX509(String alias)
    {
        return _aliasX509.get(alias);
    }

    /**
     * @return The array of protocol names to exclude from
     * {@link SSLEngine#setEnabledProtocols(String[])}
     */
    @ManagedAttribute("The excluded TLS protocols")
    public String[] getExcludeProtocols()
    {
        return _excludeProtocols.toArray(new String[0]);
    }

    /**
     * @param protocols The array of protocol names to exclude from
     * {@link SSLEngine#setEnabledProtocols(String[])}
     */
    public void setExcludeProtocols(String... protocols)
    {
        _excludeProtocols.clear();
        _excludeProtocols.addAll(Arrays.asList(protocols));
    }

    /**
     * @param protocol Protocol names to add to {@link SSLEngine#setEnabledProtocols(String[])}
     */
    public void addExcludeProtocols(String... protocol)
    {
        _excludeProtocols.addAll(Arrays.asList(protocol));
    }

    /**
     * @return The array of protocol names to include in
     * {@link SSLEngine#setEnabledProtocols(String[])}
     */
    @ManagedAttribute("The included TLS protocols")
    public String[] getIncludeProtocols()
    {
        return _includeProtocols.toArray(new String[0]);
    }

    /**
     * @param protocols The array of protocol names to include in
     * {@link SSLEngine#setEnabledProtocols(String[])}
     */
    public void setIncludeProtocols(String... protocols)
    {
        _includeProtocols.clear();
        _includeProtocols.addAll(Arrays.asList(protocols));
    }

    /**
     * @return The array of cipher suite names to exclude from
     * {@link SSLEngine#setEnabledCipherSuites(String[])}
     */
    @ManagedAttribute("The excluded cipher suites")
    public String[] getExcludeCipherSuites()
    {
        return _excludeCipherSuites.toArray(new String[0]);
    }

    /**
     * You can either use the exact cipher suite name or a a regular expression.
     *
     * @param cipherSuites The array of cipher suite names to exclude from
     * {@link SSLEngine#setEnabledCipherSuites(String[])}
     */
    public void setExcludeCipherSuites(String... cipherSuites)
    {
        _excludeCipherSuites.clear();
        _excludeCipherSuites.addAll(Arrays.asList(cipherSuites));
    }

    /**
     * @param cipher Cipher names to add to {@link SSLEngine#setEnabledCipherSuites(String[])}
     */
    public void addExcludeCipherSuites(String... cipher)
    {
        _excludeCipherSuites.addAll(Arrays.asList(cipher));
    }

    /**
     * @return The array of cipher suite names to include in
     * {@link SSLEngine#setEnabledCipherSuites(String[])}
     */
    @ManagedAttribute("The included cipher suites")
    public String[] getIncludeCipherSuites()
    {
        return _includeCipherSuites.toArray(new String[0]);
    }

    /**
     * You can either use the exact cipher suite name or a a regular expression.
     *
     * @param cipherSuites The array of cipher suite names to include in
     * {@link SSLEngine#setEnabledCipherSuites(String[])}
     */
    public void setIncludeCipherSuites(String... cipherSuites)
    {
        _includeCipherSuites.clear();
        _includeCipherSuites.addAll(Arrays.asList(cipherSuites));
    }

    @ManagedAttribute("Whether to respect the cipher suites order")
    public boolean isUseCipherSuitesOrder()
    {
        return _useCipherSuitesOrder;
    }

    public void setUseCipherSuitesOrder(boolean useCipherSuitesOrder)
    {
        _useCipherSuitesOrder = useCipherSuitesOrder;
    }

    /**
     * @return The file or URL of the SSL Key store.
     */
    @ManagedAttribute("The keyStore path")
    public String getKeyStorePath()
    {
        return Objects.toString(_keyStoreResource, null);
    }

    /**
     * @param keyStorePath The file or URL of the SSL Key store.
     */
    public void setKeyStorePath(String keyStorePath)
    {
        try
        {
            _keyStoreResource = Resource.newResource(keyStorePath);
        }
        catch (Exception e)
        {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * @return The provider of the key store
     */
    @ManagedAttribute("The keyStore provider name")
    public String getKeyStoreProvider()
    {
        return _keyStoreProvider;
    }

    /**
     * @param keyStoreProvider The provider of the key store
     */
    public void setKeyStoreProvider(String keyStoreProvider)
    {
        _keyStoreProvider = keyStoreProvider;
    }

    /**
     * @return The type of the key store (default "JKS")
     */
    @ManagedAttribute("The keyStore type")
    public String getKeyStoreType()
    {
        return (_keyStoreType);
    }

    /**
     * @param keyStoreType The type of the key store (default "JKS")
     */
    public void setKeyStoreType(String keyStoreType)
    {
        _keyStoreType = keyStoreType;
    }

    /**
     * @return Alias of SSL certificate for the connector
     */
    @ManagedAttribute("The certificate alias")
    public String getCertAlias()
    {
        return _certAlias;
    }

    /**
     * Set the default certificate Alias.
     * <p>This can be used if there are multiple non-SNI certificates
     * to specify the certificate that should be used, or with SNI
     * certificates to set a certificate to try if no others match
     * </p>
     *
     * @param certAlias Alias of SSL certificate for the connector
     */
    public void setCertAlias(String certAlias)
    {
        _certAlias = certAlias;
    }

    @ManagedAttribute("The trustStore path")
    public String getTrustStorePath()
    {
        return Objects.toString(_trustStoreResource, null);
    }

    /**
     * @param trustStorePath The file name or URL of the trust store location
     */
    public void setTrustStorePath(String trustStorePath)
    {
        try
        {
            _trustStoreResource = Resource.newResource(trustStorePath);
        }
        catch (Exception e)
        {
            throw new IllegalArgumentException(e);
        }
    }

    /**
     * @return The provider of the trust store
     */
    @ManagedAttribute("The trustStore provider name")
    public String getTrustStoreProvider()
    {
        return _trustStoreProvider;
    }

    /**
     * @param trustStoreProvider The provider of the trust store
     */
    public void setTrustStoreProvider(String trustStoreProvider)
    {
        _trustStoreProvider = trustStoreProvider;
    }

    /**
     * @return The type of the trust store
     */
    @ManagedAttribute("The trustStore type")
    public String getTrustStoreType()
    {
        return _trustStoreType;
    }

    /**
     * @param trustStoreType The type of the trust store
     */
    public void setTrustStoreType(String trustStoreType)
    {
        _trustStoreType = trustStoreType;
    }

    /**
     * @return True if SSL needs client authentication.
     * @see SSLEngine#getNeedClientAuth()
     * @deprecated use {@link Server#getNeedClientAuth()} instead
     */
    @ManagedAttribute("Whether client authentication is needed")
    @Deprecated
    public boolean getNeedClientAuth()
    {
        return _needClientAuth;
    }

    /**
     * @param needClientAuth True if SSL needs client authentication.
     * @see SSLEngine#getNeedClientAuth()
     * @deprecated use {@link Server#setNeedClientAuth(boolean)} instead
     */
    @Deprecated
    public void setNeedClientAuth(boolean needClientAuth)
    {
        _needClientAuth = needClientAuth;
    }

    /**
     * @return True if SSL wants client authentication.
     * @see SSLEngine#getWantClientAuth()
     * @deprecated use {@link Server#getWantClientAuth()} instead
     */
    @ManagedAttribute("Whether client authentication is wanted")
    @Deprecated
    public boolean getWantClientAuth()
    {
        return _wantClientAuth;
    }

    /**
     * @param wantClientAuth True if SSL wants client authentication.
     * @see SSLEngine#getWantClientAuth()
     * @deprecated use {@link Server#setWantClientAuth(boolean)} instead
     */
    @Deprecated
    public void setWantClientAuth(boolean wantClientAuth)
    {
        _wantClientAuth = wantClientAuth;
    }

    /**
     * @return true if SSL certificate has to be validated
     */
    @ManagedAttribute("Whether certificates are validated")
    public boolean isValidateCerts()
    {
        return _validateCerts;
    }

    /**
     * @param validateCerts true if SSL certificates have to be validated
     */
    public void setValidateCerts(boolean validateCerts)
    {
        _validateCerts = validateCerts;
    }

    /**
     * @return true if SSL certificates of the peer have to be validated
     */
    @ManagedAttribute("Whether peer certificates are validated")
    public boolean isValidatePeerCerts()
    {
        return _validatePeerCerts;
    }

    /**
     * @param validatePeerCerts true if SSL certificates of the peer have to be validated
     */
    public void setValidatePeerCerts(boolean validatePeerCerts)
    {
        _validatePeerCerts = validatePeerCerts;
    }

    /**
     * @param password The password for the key store.  If null is passed and
     * a keystore is set, then
     * the {@link #getPassword(String)} is used to
     * obtain a password either from the {@value #PASSWORD_PROPERTY}
     * system property or by prompting for manual entry.
     */
    public void setKeyStorePassword(String password)
    {
        if (password == null)
        {
            if (_keyStoreResource != null)
                _keyStorePassword = getPassword(PASSWORD_PROPERTY);
            else
                _keyStorePassword = null;
        }
        else
        {
            _keyStorePassword = newPassword(password);
        }
    }

    /**
     * @param password The password (if any) for the specific key within the key store.
     * If null is passed and the {@value #KEYPASSWORD_PROPERTY} system property is set,
     * then the {@link #getPassword(String)} is used to
     * obtain a password from the {@value #KEYPASSWORD_PROPERTY} system property.
     */
    public void setKeyManagerPassword(String password)
    {
        if (password == null)
        {
            if (System.getProperty(KEYPASSWORD_PROPERTY) != null)
                _keyManagerPassword = getPassword(KEYPASSWORD_PROPERTY);
            else
                _keyManagerPassword = null;
        }
        else
        {
            _keyManagerPassword = newPassword(password);
        }
    }

    /**
     * @param password The password for the truststore. If null is passed and a truststore is set
     * that is different from the keystore, then
     * the {@link #getPassword(String)} is used to
     * obtain a password either from the {@value #PASSWORD_PROPERTY}
     * system property or by prompting for manual entry.
     */
    public void setTrustStorePassword(String password)
    {
        if (password == null)
        {
            if (_trustStoreResource != null && !_trustStoreResource.equals(_keyStoreResource))
                _trustStorePassword = getPassword(PASSWORD_PROPERTY);
            else
                _trustStorePassword = null;
        }
        else
        {
            _trustStorePassword = newPassword(password);
        }
    }

    /**
     * <p>
     * Get the optional Security Provider name.
     * </p>
     * <p>
     * Security Provider name used with:
     * </p>
     * <ul>
     * <li>{@link SecureRandom#getInstance(String, String)}</li>
     * <li>{@link SSLContext#getInstance(String, String)}</li>
     * <li>{@link TrustManagerFactory#getInstance(String, String)}</li>
     * <li>{@link KeyManagerFactory#getInstance(String, String)}</li>
     * <li>{@link CertStore#getInstance(String, CertStoreParameters, String)}</li>
     * <li>{@link java.security.cert.CertificateFactory#getInstance(String, String)}</li>
     * </ul>
     *
     * @return The optional Security Provider name.
     */
    @ManagedAttribute("The provider name")
    public String getProvider()
    {
        return _sslProvider;
    }

    /**
     * <p>
     * Set the optional Security Provider name.
     * </p>
     * <p>
     * Security Provider name used with:
     * </p>
     * <ul>
     * <li>{@link SecureRandom#getInstance(String, String)}</li>
     * <li>{@link SSLContext#getInstance(String, String)}</li>
     * <li>{@link TrustManagerFactory#getInstance(String, String)}</li>
     * <li>{@link KeyManagerFactory#getInstance(String, String)}</li>
     * <li>{@link CertStore#getInstance(String, CertStoreParameters, String)}</li>
     * <li>{@link java.security.cert.CertificateFactory#getInstance(String, String)}</li>
     * </ul>
     *
     * @param provider The optional Security Provider name.
     */
    public void setProvider(String provider)
    {
        _sslProvider = provider;
    }

    /**
     * @return The SSL protocol (default "TLS") passed to
     * {@link SSLContext#getInstance(String, String)}
     */
    @ManagedAttribute("The TLS protocol")
    public String getProtocol()
    {
        return _sslProtocol;
    }

    /**
     * @param protocol The SSL protocol (default "TLS") passed to
     * {@link SSLContext#getInstance(String, String)}
     */
    public void setProtocol(String protocol)
    {
        _sslProtocol = protocol;
    }

    /**
     * @return The algorithm name, which if set is passed to
     * {@link SecureRandom#getInstance(String)} to obtain the {@link SecureRandom} instance passed to
     * {@link SSLContext#init(javax.net.ssl.KeyManager[], javax.net.ssl.TrustManager[], SecureRandom)}
     */
    @ManagedAttribute("The SecureRandom algorithm")
    public String getSecureRandomAlgorithm()
    {
        return _secureRandomAlgorithm;
    }

    /**
     * @param algorithm The algorithm name, which if set is passed to
     * {@link SecureRandom#getInstance(String)} to obtain the {@link SecureRandom} instance passed to
     * {@link SSLContext#init(javax.net.ssl.KeyManager[], javax.net.ssl.TrustManager[], SecureRandom)}
     */
    public void setSecureRandomAlgorithm(String algorithm)
    {
        _secureRandomAlgorithm = algorithm;
    }

    /**
     * @return The algorithm name (default "SunX509") used by the {@link KeyManagerFactory}
     */
    @ManagedAttribute("The KeyManagerFactory algorithm")
    public String getKeyManagerFactoryAlgorithm()
    {
        return _keyManagerFactoryAlgorithm;
    }

    /**
     * @param algorithm The algorithm name (default "SunX509") used by the {@link KeyManagerFactory}
     */
    public void setKeyManagerFactoryAlgorithm(String algorithm)
    {
        _keyManagerFactoryAlgorithm = algorithm;
    }

    /**
     * @return The algorithm name (default "SunX509") used by the {@link TrustManagerFactory}
     */
    @ManagedAttribute("The TrustManagerFactory algorithm")
    public String getTrustManagerFactoryAlgorithm()
    {
        return _trustManagerFactoryAlgorithm;
    }

    /**
     * @return True if all certificates should be trusted if there is no KeyStore or TrustStore
     */
    @ManagedAttribute("Whether certificates should be trusted even if they are invalid")
    public boolean isTrustAll()
    {
        return _trustAll;
    }

    /**
     * @param trustAll True if all certificates should be trusted if there is no KeyStore or TrustStore
     */
    public void setTrustAll(boolean trustAll)
    {
        _trustAll = trustAll;
        if (trustAll)
            setEndpointIdentificationAlgorithm(null);
    }

    /**
     * @param algorithm The algorithm name (default "SunX509") used by the {@link TrustManagerFactory}
     * Use the string "TrustAll" to install a trust manager that trusts all.
     */
    public void setTrustManagerFactoryAlgorithm(String algorithm)
    {
        _trustManagerFactoryAlgorithm = algorithm;
    }

    /**
     * @return whether TLS renegotiation is allowed (true by default)
     */
    @ManagedAttribute("Whether renegotiation is allowed")
    public boolean isRenegotiationAllowed()
    {
        return _renegotiationAllowed;
    }

    /**
     * @param renegotiationAllowed whether TLS renegotiation is allowed
     */
    public void setRenegotiationAllowed(boolean renegotiationAllowed)
    {
        _renegotiationAllowed = renegotiationAllowed;
    }

    /**
     * @return The number of renegotiations allowed for this connection.  When the limit
     * is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
     */
    @ManagedAttribute("The max number of renegotiations allowed")
    public int getRenegotiationLimit()
    {
        return _renegotiationLimit;
    }

    /**
     * @param renegotiationLimit The number of renegotions allowed for this connection.
     * When the limit is 0 renegotiation will be denied. If the limit is less than 0 then no limit is applied.
     * Default 5.
     */
    public void setRenegotiationLimit(int renegotiationLimit)
    {
        _renegotiationLimit = renegotiationLimit;
    }

    /**
     * @return Path to file that contains Certificate Revocation List
     */
    @ManagedAttribute("The path to the certificate revocation list file")
    public String getCrlPath()
    {
        return _crlPath;
    }

    /**
     * @param crlPath Path to file that contains Certificate Revocation List
     */
    public void setCrlPath(String crlPath)
    {
        _crlPath = crlPath;
    }

    /**
     * @return Maximum number of intermediate certificates in
     * the certification path (-1 for unlimited)
     */
    @ManagedAttribute("The maximum number of intermediate certificates")
    public int getMaxCertPathLength()
    {
        return _maxCertPathLength;
    }

    /**
     * @param maxCertPathLength maximum number of intermediate certificates in
     * the certification path (-1 for unlimited)
     */
    public void setMaxCertPathLength(int maxCertPathLength)
    {
        _maxCertPathLength = maxCertPathLength;
    }

    /**
     * @return The SSLContext
     */
    public SSLContext getSslContext()
    {
        if (!isStarted())
            return _setContext;

        synchronized (this)
        {
            return _factory._context;
        }
    }

    /**
     * @param sslContext Set a preconfigured SSLContext
     */
    public void setSslContext(SSLContext sslContext)
    {
        _setContext = sslContext;
    }

    /**
     * @return the endpoint identification algorithm
     */
    @ManagedAttribute("The endpoint identification algorithm")
    public String getEndpointIdentificationAlgorithm()
    {
        return _endpointIdentificationAlgorithm;
    }

    /**
     * When set to "HTTPS" hostname verification will be enabled.
     * Deployments can be vulnerable to a man-in-the-middle attack if a EndpointIndentificationAlgorithm
     * is not set.
     *
     * @param endpointIdentificationAlgorithm Set the endpointIdentificationAlgorithm
     * @see #setHostnameVerifier(HostnameVerifier)
     */
    public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm)
    {
        _endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;
    }

    public PKIXCertPathChecker getPkixCertPathChecker()
    {
        return _pkixCertPathChecker;
    }

    public void setPkixCertPathChecker(PKIXCertPathChecker pkixCertPatchChecker)
    {
        _pkixCertPathChecker = pkixCertPatchChecker;
    }

    /**
     * Override this method to provide alternate way to load a keystore.
     *
     * @param resource the resource to load the keystore from
     * @return the key store instance
     * @throws Exception if the keystore cannot be loaded
     */
    protected KeyStore loadKeyStore(Resource resource) throws Exception
    {
        String storePassword = Objects.toString(_keyStorePassword, null);
        return CertificateUtils.getKeyStore(resource, getKeyStoreType(), getKeyStoreProvider(), storePassword);
    }

    /**
     * Override this method to provide alternate way to load a truststore.
     *
     * @param resource the resource to load the truststore from
     * @return the key store instance
     * @throws Exception if the truststore cannot be loaded
     */
    protected KeyStore loadTrustStore(Resource resource) throws Exception
    {
        String type = Objects.toString(getTrustStoreType(), getKeyStoreType());
        String provider = Objects.toString(getTrustStoreProvider(), getKeyStoreProvider());
        Password passwd = _trustStorePassword;
        if (resource == null || resource.equals(_keyStoreResource))
        {
            resource = _keyStoreResource;
            if (passwd == null)
                passwd = _keyStorePassword;
        }
        return CertificateUtils.getKeyStore(resource, type, provider, Objects.toString(passwd, null));
    }

    /**
     * Loads certificate revocation list (CRL) from a file.
     * <p>
     * Required for integrations to be able to override the mechanism used to
     * load CRL in order to provide their own implementation.
     *
     * @param crlPath path of certificate revocation list file
     * @return Collection of CRL's
     * @throws Exception if the certificate revocation list cannot be loaded
     */
    protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception
    {
        return CertificateUtils.loadCRL(crlPath);
    }

    protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception
    {
        KeyManager[] managers = null;

        if (keyStore != null)
        {
            KeyManagerFactory keyManagerFactory = getKeyManagerFactoryInstance();
            keyManagerFactory.init(keyStore, _keyManagerPassword == null ? (_keyStorePassword == null ? null : _keyStorePassword.toString().toCharArray()) : _keyManagerPassword.toString().toCharArray());
            managers = keyManagerFactory.getKeyManagers();

            if (managers != null)
            {
                String alias = getCertAlias();
                if (alias != null)
                {
                    for (int idx = 0; idx < managers.length; idx++)
                    {
                        if (managers[idx] instanceof X509ExtendedKeyManager)
                            managers[idx] = new AliasedX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx], alias);
                    }
                }

                // Is SNI needed to select a certificate?
                if (!_certWilds.isEmpty() || _certHosts.size() > 1 || (_certHosts.size() == 1 && _aliasX509.size() > 1))
                {
                    for (int idx = 0; idx < managers.length; idx++)
                    {
                        if (managers[idx] instanceof X509ExtendedKeyManager)
                            managers[idx] = new SniX509ExtendedKeyManager((X509ExtendedKeyManager)managers[idx]);
                    }
                }
            }
        }

        if (LOG.isDebugEnabled())
            LOG.debug("managers={} for {}", managers, this);

        return managers;
    }

    protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
    {
        TrustManager[] managers = null;
        if (trustStore != null)
        {
            // Revocation checking is only supported for PKIX algorithm
            if (isValidatePeerCerts() && "PKIX".equalsIgnoreCase(getTrustManagerFactoryAlgorithm()))
            {
                PKIXBuilderParameters pbParams = newPKIXBuilderParameters(trustStore, crls);

                TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance();
                trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams));

                managers = trustManagerFactory.getTrustManagers();
            }
            else
            {
                TrustManagerFactory trustManagerFactory = getTrustManagerFactoryInstance();
                trustManagerFactory.init(trustStore);

                managers = trustManagerFactory.getTrustManagers();
            }
        }

        return managers;
    }

    protected PKIXBuilderParameters newPKIXBuilderParameters(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception
    {
        PKIXBuilderParameters pbParams = new PKIXBuilderParameters(trustStore, new X509CertSelector());

        // Set maximum certification path length
        pbParams.setMaxPathLength(_maxCertPathLength);

        // Make sure revocation checking is enabled
        pbParams.setRevocationEnabled(true);

        if (_pkixCertPathChecker != null)
            pbParams.addCertPathChecker(_pkixCertPathChecker);

        if (crls != null && !crls.isEmpty())
        {
            pbParams.addCertStore(getCertStoreInstance(crls));
        }

        if (_enableCRLDP)
        {
            // Enable Certificate Revocation List Distribution Points (CRLDP) support
            System.setProperty("com.sun.security.enableCRLDP", "true");
        }

        if (_enableOCSP)
        {
            // Enable On-Line Certificate Status Protocol (OCSP) support
            Security.setProperty("ocsp.enable", "true");

            if (_ocspResponderURL != null)
            {
                // Override location of OCSP Responder
                Security.setProperty("ocsp.responderURL", _ocspResponderURL);
            }
        }

        return pbParams;
    }

    /**
     * Select protocols to be used by the connector
     * based on configured inclusion and exclusion lists
     * as well as enabled and supported protocols.
     *
     * @param enabledProtocols Array of enabled protocols
     * @param supportedProtocols Array of supported protocols
     */
    public void selectProtocols(String[] enabledProtocols, String[] supportedProtocols)
    {
        Set<String> selectedProtocols = new LinkedHashSet<>();

        // Set the starting protocols - either from the included or enabled list
        if (!_includeProtocols.isEmpty())
        {
            // Use only the supported included protocols
            for (String protocol : _includeProtocols)
            {
                if (Arrays.asList(supportedProtocols).contains(protocol))
                    selectedProtocols.add(protocol);
                else
                    LOG.info("Protocol {} not supported in {}", protocol, Arrays.asList(supportedProtocols));
            }
        }
        else
            selectedProtocols.addAll(Arrays.asList(enabledProtocols));

        // Remove any excluded protocols
        selectedProtocols.removeAll(_excludeProtocols);

        if (selectedProtocols.isEmpty())
            LOG.warn("No selected protocols from {}", Arrays.asList(supportedProtocols));

        _selectedProtocols = selectedProtocols.toArray(new String[0]);
    }

    /**
     * Select cipher suites to be used by the connector
     * based on configured inclusion and exclusion lists
     * as well as enabled and supported cipher suite lists.
     *
     * @param enabledCipherSuites Array of enabled cipher suites
     * @param supportedCipherSuites Array of supported cipher suites
     */
    protected void selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites)
    {
        List<String> selectedCiphers = new ArrayList<>();

        // Set the starting ciphers - either from the included or enabled list
        if (_includeCipherSuites.isEmpty())
            selectedCiphers.addAll(Arrays.asList(enabledCipherSuites));
        else
            processIncludeCipherSuites(supportedCipherSuites, selectedCiphers);

        removeExcludedCipherSuites(selectedCiphers);

        if (selectedCiphers.isEmpty())
            LOG.warn("No supported ciphers from {}", Arrays.asList(supportedCipherSuites));

        Comparator<String> comparator = getCipherComparator();
        if (comparator != null)
        {
            if (LOG.isDebugEnabled())
                LOG.debug("Sorting selected ciphers with {}", comparator);
            selectedCiphers.sort(comparator);
        }

        _selectedCipherSuites = selectedCiphers.toArray(new String[0]);
    }

    protected void processIncludeCipherSuites(String[] supportedCipherSuites, List<String> selectedCiphers)
    {
        for (String cipherSuite : _includeCipherSuites)
        {
            Pattern p = Pattern.compile(cipherSuite);
            boolean added = false;
            for (String supportedCipherSuite : supportedCipherSuites)
            {
                Matcher m = p.matcher(supportedCipherSuite);
                if (m.matches())
                {
                    added = true;
                    selectedCiphers.add(supportedCipherSuite);
                }
            }
            if (!added)
                LOG.info("No Cipher matching '{}' is supported", cipherSuite);
        }
    }

    protected void removeExcludedCipherSuites(List<String> selectedCiphers)
    {
        for (String excludeCipherSuite : _excludeCipherSuites)
        {
            Pattern excludeCipherPattern = Pattern.compile(excludeCipherSuite);
            for (Iterator<String> i = selectedCiphers.iterator(); i.hasNext(); )
            {
                String selectedCipherSuite = i.next();
                Matcher m = excludeCipherPattern.matcher(selectedCipherSuite);
                if (m.matches())
                    i.remove();
            }
        }
    }

    /**
     * Check if the lifecycle has been started and throw runtime exception
     */
    private void checkIsStarted()
    {
        if (!isStarted())
            throw new IllegalStateException("!STARTED: " + this);
    }

    /**
     * @return true if CRL Distribution Points support is enabled
     */
    @ManagedAttribute("Whether certificate revocation list distribution points is enabled")
    public boolean isEnableCRLDP()
    {
        return _enableCRLDP;
    }

    /**
     * Enables CRL Distribution Points Support
     *
     * @param enableCRLDP true - turn on, false - turns off
     */
    public void setEnableCRLDP(boolean enableCRLDP)
    {
        _enableCRLDP = enableCRLDP;
    }

    /**
     * @return true if On-Line Certificate Status Protocol support is enabled
     */
    @ManagedAttribute("Whether online certificate status protocol support is enabled")
    public boolean isEnableOCSP()
    {
        return _enableOCSP;
    }

    /**
     * Enables On-Line Certificate Status Protocol support
     *
     * @param enableOCSP true - turn on, false - turn off
     */
    public void setEnableOCSP(boolean enableOCSP)
    {
        _enableOCSP = enableOCSP;
    }

    /**
     * @return Location of the OCSP Responder
     */
    @ManagedAttribute("The online certificate status protocol URL")
    public String getOcspResponderURL()
    {
        return _ocspResponderURL;
    }

    /**
     * Set the location of the OCSP Responder.
     *
     * @param ocspResponderURL location of the OCSP Responder
     */
    public void setOcspResponderURL(String ocspResponderURL)
    {
        _ocspResponderURL = ocspResponderURL;
    }

    /**
     * Set the key store.
     *
     * @param keyStore the key store to set
     */
    public void setKeyStore(KeyStore keyStore)
    {
        _setKeyStore = keyStore;
    }

    public KeyStore getKeyStore()
    {
        if (!isStarted())
            return _setKeyStore;

        synchronized (this)
        {
            return _factory._keyStore;
        }
    }

    /**
     * Set the trust store.
     *
     * @param trustStore the trust store to set
     */
    public void setTrustStore(KeyStore trustStore)
    {
        _setTrustStore = trustStore;
    }

    public KeyStore getTrustStore()
    {
        if (!isStarted())
            return _setTrustStore;

        synchronized (this)
        {
            return _factory._trustStore;
        }
    }

    /**
     * Set the key store resource.
     *
     * @param resource the key store resource to set
     */
    public void setKeyStoreResource(Resource resource)
    {
        _keyStoreResource = resource;
    }

    public Resource getKeyStoreResource()
    {
        return _keyStoreResource;
    }

    /**
     * Set the trust store resource.
     *
     * @param resource the trust store resource to set
     */
    public void setTrustStoreResource(Resource resource)
    {
        _trustStoreResource = resource;
    }

    public Resource getTrustStoreResource()
    {
        return _trustStoreResource;
    }

    /**
     * @return true if SSL Session caching is enabled
     */
    @ManagedAttribute("Whether TLS session caching is enabled")
    public boolean isSessionCachingEnabled()
    {
        return _sessionCachingEnabled;
    }

    /**
     * Set the flag to enable SSL Session caching.
     * If set to true, then the {@link SSLContext#createSSLEngine(String, int)} method is
     * used to pass host and port information as a hint for session reuse.  Note that
     * this is only a hint and session may not be reused. Moreover, the hint is typically
     * only used on client side implementations and setting this to false does not
     * stop a server from accepting an offered session ID to reuse.
     *
     * @param enableSessionCaching the value of the flag
     */
    public void setSessionCachingEnabled(boolean enableSessionCaching)
    {
        _sessionCachingEnabled = enableSessionCaching;
    }

    /**
     * Get SSL session cache size.
     * Passed directly to {@link SSLSessionContext#setSessionCacheSize(int)}
     *
     * @return SSL session cache size
     */
    @ManagedAttribute("The maximum TLS session cache size")
    public int getSslSessionCacheSize()
    {
        return _sslSessionCacheSize;
    }

    /**
     * Set SSL session cache size.
     * <p>Set the max cache size to be set on {@link SSLSessionContext#setSessionCacheSize(int)}
     * when this factory is started.</p>
     *
     * @param sslSessionCacheSize SSL session cache size to set. A value  of -1 (default) uses
     * the JVM default, 0 means unlimited and positive number is a max size.
     */
    public void setSslSessionCacheSize(int sslSessionCacheSize)
    {
        _sslSessionCacheSize = sslSessionCacheSize;
    }

    /**
     * Get SSL session timeout.
     *
     * @return SSL session timeout
     */
    @ManagedAttribute("The TLS session cache timeout, in seconds")
    public int getSslSessionTimeout()
    {
        return _sslSessionTimeout;
    }

    /**
     * Set SSL session timeout.
     * <p>Set the timeout in seconds to be set on {@link SSLSessionContext#setSessionTimeout(int)}
     * when this factory is started.</p>
     *
     * @param sslSessionTimeout SSL session timeout to set in seconds. A value of -1 (default) uses
     * the JVM default, 0 means unlimited and positive number is a timeout in seconds.
     */
    public void setSslSessionTimeout(int sslSessionTimeout)
    {
        _sslSessionTimeout = sslSessionTimeout;
    }

    /**
     * @return the HostnameVerifier used by a client to verify host names in the server certificate
     */
    public HostnameVerifier getHostnameVerifier()
    {
        return _hostnameVerifier;
    }

    /**
     * <p>Sets a {@code HostnameVerifier} used by a client to verify host names in the server certificate.</p>
     * <p>The {@code HostnameVerifier} works in conjunction with {@link #setEndpointIdentificationAlgorithm(String)}.</p>
     * <p>When {@code endpointIdentificationAlgorithm=="HTTPS"} (the default) the JDK TLS implementation
     * checks that the host name indication set by the client matches the host names in the server certificate.
     * If this check passes successfully, the {@code HostnameVerifier} is invoked and the application
     * can perform additional checks and allow/deny the connection to the server.</p>
     * <p>When {@code endpointIdentificationAlgorithm==null} the JDK TLS implementation will not check
     * the host names, and any check is therefore performed only by the {@code HostnameVerifier.}</p>
     *
     * @param hostnameVerifier the HostnameVerifier used by a client to verify host names in the server certificate
     */
    public void setHostnameVerifier(HostnameVerifier hostnameVerifier)
    {
        _hostnameVerifier = hostnameVerifier;
    }

    /**
     * Returns the password object for the given realm.
     *
     * @param realm the realm
     * @return the Password object
     */
    protected Password getPassword(String realm)
    {
        return Password.getPassword(realm, null, null);
    }

    /**
     * Creates a new Password object.
     *
     * @param password the password string
     * @return the new Password object
     */
    public Password newPassword(String password)
    {
        return new Password(password);
    }

    public SSLServerSocket newSslServerSocket(String host, int port, int backlog) throws IOException
    {
        checkIsStarted();

        SSLContext context = getSslContext();
        SSLServerSocketFactory factory = context.getServerSocketFactory();
        SSLServerSocket socket =
            (SSLServerSocket)(host == null
                ? factory.createServerSocket(port, backlog)
                : factory.createServerSocket(port, backlog, InetAddress.getByName(host)));
        socket.setSSLParameters(customize(socket.getSSLParameters()));

        return socket;
    }

    public SSLSocket newSslSocket() throws IOException
    {
        checkIsStarted();

        SSLContext context = getSslContext();
        SSLSocketFactory factory = context.getSocketFactory();
        SSLSocket socket = (SSLSocket)factory.createSocket();
        socket.setSSLParameters(customize(socket.getSSLParameters()));

        return socket;
    }

    protected CertificateFactory getCertificateFactoryInstance(String type) throws CertificateException
    {
        String provider = getProvider();

        try
        {
            if (provider != null)
            {
                return CertificateFactory.getInstance(type, provider);
            }
        }
        catch (Throwable cause)
        {
            LOG.info("Unable to get CertificateFactory instance for type [{}] on provider [{}], using default", type, provider);
            if (LOG.isDebugEnabled())
                LOG.debug(cause);
        }

        return CertificateFactory.getInstance(type);
    }

    protected CertStore getCertStoreInstance(Collection<? extends CRL> crls) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException
    {
        String type = "Collection";
        String provider = getProvider();

        try
        {
            if (provider != null)
            {
                return CertStore.getInstance(type, new CollectionCertStoreParameters(crls), provider);
            }
        }
        catch (Throwable cause)
        {
            LOG.info("Unable to get CertStore instance for type [{}] on provider [{}], using default", type, provider);
            if (LOG.isDebugEnabled())
                LOG.debug(cause);
        }

        return CertStore.getInstance(type, new CollectionCertStoreParameters(crls));
    }

    protected KeyManagerFactory getKeyManagerFactoryInstance() throws NoSuchAlgorithmException
    {
        String algorithm = getKeyManagerFactoryAlgorithm();
        String provider = getProvider();

        try
        {
            if (provider != null)
            {
                return KeyManagerFactory.getInstance(algorithm, provider);
            }
        }
        catch (Throwable cause)
        {
            // fall back to non-provider option
            LOG.info("Unable to get KeyManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
            if (LOG.isDebugEnabled())
                LOG.debug(cause);
        }

        return KeyManagerFactory.getInstance(algorithm);
    }

    protected SecureRandom getSecureRandomInstance() throws NoSuchAlgorithmException
    {
        String algorithm = getSecureRandomAlgorithm();

        if (algorithm != null)
        {
            String provider = getProvider();

            try
            {
                if (provider != null)
                {
                    return SecureRandom.getInstance(algorithm, provider);
                }
            }
            catch (Throwable cause)
            {
                LOG.info("Unable to get SecureRandom instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
                if (LOG.isDebugEnabled())
                    LOG.debug(cause);
            }

            return SecureRandom.getInstance(algorithm);
        }

        return null;
    }

    protected SSLContext getSSLContextInstance() throws NoSuchAlgorithmException
    {
        String protocol = getProtocol();
        String provider = getProvider();

        try
        {
            if (provider != null)
            {
                return SSLContext.getInstance(protocol, provider);
            }
        }
        catch (Throwable cause)
        {
            LOG.info("Unable to get SSLContext instance for protocol [{}] on provider [{}], using default", protocol, provider);
            if (LOG.isDebugEnabled())
                LOG.debug(cause);
        }

        return SSLContext.getInstance(protocol);
    }

    protected TrustManagerFactory getTrustManagerFactoryInstance() throws NoSuchAlgorithmException
    {
        String algorithm = getTrustManagerFactoryAlgorithm();
        String provider = getProvider();
        try
        {
            if (provider != null)
            {
                return TrustManagerFactory.getInstance(algorithm, provider);
            }
        }
        catch (Throwable cause)
        {
            LOG.info("Unable to get TrustManagerFactory instance for algorithm [{}] on provider [{}], using default", algorithm, provider);
            if (LOG.isDebugEnabled())
            {
                LOG.debug(cause);
            }
        }

        return TrustManagerFactory.getInstance(algorithm);
    }

    /**
     * Factory method for "scratch" {@link SSLEngine}s, usually only used for retrieving configuration
     * information such as the application buffer size or the list of protocols/ciphers.
     * <p>
     * This method should not be used for creating {@link SSLEngine}s that are used in actual socket
     * communication.
     *
     * @return a new, "scratch" {@link SSLEngine}
     */
    public SSLEngine newSSLEngine()
    {
        checkIsStarted();

        SSLContext context = getSslContext();
        SSLEngine sslEngine = context.createSSLEngine();
        customize(sslEngine);

        return sslEngine;
    }

    /**
     * General purpose factory method for creating {@link SSLEngine}s, although creation of
     * {@link SSLEngine}s on the server-side should prefer {@link #newSSLEngine(InetSocketAddress)}.
     *
     * @param host the remote host
     * @param port the remote port
     * @return a new {@link SSLEngine}
     */
    public SSLEngine newSSLEngine(String host, int port)
    {
        checkIsStarted();

        SSLContext context = getSslContext();
        SSLEngine sslEngine = isSessionCachingEnabled()
            ? context.createSSLEngine(host, port)
            : context.createSSLEngine();
        customize(sslEngine);

        return sslEngine;
    }

    /**
     * Server-side only factory method for creating {@link SSLEngine}s.
     * <p>
     * If the given {@code address} is null, it is equivalent to {@link #newSSLEngine()}, otherwise
     * {@link #newSSLEngine(String, int)} is called.
     * <p>
     * Clients that wish to create {@link SSLEngine} instances must use {@link #newSSLEngine(String, int)}.
     *
     * @param address the remote peer address
     * @return a new {@link SSLEngine}
     */
    public SSLEngine newSSLEngine(InetSocketAddress address)
    {
        if (address == null)
            return newSSLEngine();
        return newSSLEngine(address.getHostString(), address.getPort());
    }

    /**
     * Customize an SslEngine instance with the configuration of this factory,
     * by calling {@link #customize(SSLParameters)}
     *
     * @param sslEngine the SSLEngine to customize
     */
    public void customize(SSLEngine sslEngine)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Customize {}", sslEngine);

        sslEngine.setSSLParameters(customize(sslEngine.getSSLParameters()));
    }

    /**
     * Customize an SslParameters instance with the configuration of this factory.
     *
     * @param sslParams The parameters to customize
     * @return The passed instance of sslParams (returned as a convenience)
     */
    public SSLParameters customize(SSLParameters sslParams)
    {
        sslParams.setEndpointIdentificationAlgorithm(getEndpointIdentificationAlgorithm());
        sslParams.setUseCipherSuitesOrder(isUseCipherSuitesOrder());
        if (!_certHosts.isEmpty() || !_certWilds.isEmpty())
            sslParams.setSNIMatchers(Collections.singletonList(new AliasSNIMatcher()));
        if (_selectedCipherSuites != null)
            sslParams.setCipherSuites(_selectedCipherSuites);
        if (_selectedProtocols != null)
            sslParams.setProtocols(_selectedProtocols);
        if (!(this instanceof Client))
        {
            if (getWantClientAuth())
                sslParams.setWantClientAuth(true);
            if (getNeedClientAuth())
                sslParams.setNeedClientAuth(true);
        }
        return sslParams;
    }

    public void reload(Consumer<SslContextFactory> consumer) throws Exception
    {
        synchronized (this)
        {
            consumer.accept(this);
            unload();
            load();
        }
    }

    /**
     * Obtain the X509 Certificate Chain from the provided SSLSession using this
     * SslContextFactory's optional Provider specific {@link CertificateFactory}.
     *
     * @param sslSession the session to use for active peer certificates
     * @return the certificate chain
     */
    public X509Certificate[] getX509CertChain(SSLSession sslSession)
    {
        return getX509CertChain(this, sslSession);
    }

    /**
     * Obtain the X509 Certificate Chain from the provided SSLSession using the
     * default {@link CertificateFactory} behaviors
     *
     * @param sslSession the session to use for active peer certificates
     * @return the certificate chain
     */
    public static X509Certificate[] getCertChain(SSLSession sslSession)
    {
        return getX509CertChain(null, sslSession);
    }

    private static X509Certificate[] getX509CertChain(SslContextFactory sslContextFactory, SSLSession sslSession)
    {
        try
        {
            Certificate[] javaxCerts = sslSession.getPeerCertificates();
            if (javaxCerts == null || javaxCerts.length == 0)
                return null;

            int length = javaxCerts.length;
            X509Certificate[] javaCerts = new X509Certificate[length];

            String type = "X.509";
            CertificateFactory cf;
            if (sslContextFactory != null)
            {
                cf = sslContextFactory.getCertificateFactoryInstance(type);
            }
            else
            {
                cf = CertificateFactory.getInstance(type);
            }

            for (int i = 0; i < length; i++)
            {
                byte[] bytes = javaxCerts[i].getEncoded();
                ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
                javaCerts[i] = (X509Certificate)cf.generateCertificate(stream);
            }

            return javaCerts;
        }
        catch (SSLPeerUnverifiedException pue)
        {
            return null;
        }
        catch (Exception e)
        {
            LOG.warn(Log.EXCEPTION, e);
            return null;
        }
    }

    /**
     * Given the name of a TLS/SSL cipher suite, return an int representing it effective stream
     * cipher key strength. i.e. How much entropy material is in the key material being fed into the
     * encryption routines.
     * <p>
     * This is based on the information on effective key lengths in RFC 2246 - The TLS Protocol
     * Version 1.0, Appendix C. CipherSuite definitions:
     * <pre>
     *                         Effective
     *     Cipher       Type    Key Bits
     *
     *     NULL       * Stream     0
     *     IDEA_CBC     Block    128
     *     RC2_CBC_40 * Block     40
     *     RC4_40     * Stream    40
     *     RC4_128      Stream   128
     *     DES40_CBC  * Block     40
     *     DES_CBC      Block     56
     *     3DES_EDE_CBC Block    168
     * </pre>
     *
     * @param cipherSuite String name of the TLS cipher suite.
     * @return int indicating the effective key entropy bit-length.
     */
    public static int deduceKeyLength(String cipherSuite)
    {
        // Roughly ordered from most common to least common.
        if (cipherSuite == null)
            return 0;
        else if (cipherSuite.contains("WITH_AES_256_"))
            return 256;
        else if (cipherSuite.contains("WITH_RC4_128_"))
            return 128;
        else if (cipherSuite.contains("WITH_AES_128_"))
            return 128;
        else if (cipherSuite.contains("WITH_RC4_40_"))
            return 40;
        else if (cipherSuite.contains("WITH_3DES_EDE_CBC_"))
            return 168;
        else if (cipherSuite.contains("WITH_IDEA_CBC_"))
            return 128;
        else if (cipherSuite.contains("WITH_RC2_CBC_40_"))
            return 40;
        else if (cipherSuite.contains("WITH_DES40_CBC_"))
            return 40;
        else if (cipherSuite.contains("WITH_DES_CBC_"))
            return 56;
        else
            return 0;
    }

    @Override
    public String toString()
    {
        return String.format("%s@%x[provider=%s,keyStore=%s,trustStore=%s]",
            getClass().getSimpleName(),
            hashCode(),
            _sslProvider,
            _keyStoreResource,
            _trustStoreResource);
    }

    class Factory
    {
        private final KeyStore _keyStore;
        private final KeyStore _trustStore;
        private final SSLContext _context;

        Factory(KeyStore keyStore, KeyStore trustStore, SSLContext context)
        {
            super();
            _keyStore = keyStore;
            _trustStore = trustStore;
            _context = context;
        }
    }

    class AliasSNIMatcher extends SNIMatcher
    {
        private String _host;
        private X509 _x509;

        AliasSNIMatcher()
        {
            super(StandardConstants.SNI_HOST_NAME);
        }

        @Override
        public boolean matches(SNIServerName serverName)
        {
            if (LOG.isDebugEnabled())
                LOG.debug("SNI matching for {}", serverName);

            if (serverName instanceof SNIHostName)
            {
                String host = _host = ((SNIHostName)serverName).getAsciiName();
                host = StringUtil.asciiToLowerCase(host);

                // Try an exact match
                _x509 = _certHosts.get(host);

                // Else try an exact wild match
                if (_x509 == null)
                {
                    _x509 = _certWilds.get(host);

                    // Else try an 1 deep wild match
                    if (_x509 == null)
                    {
                        int dot = host.indexOf('.');
                        if (dot >= 0)
                        {
                            String domain = host.substring(dot + 1);
                            _x509 = _certWilds.get(domain);
                        }
                    }
                }

                if (LOG.isDebugEnabled())
                    LOG.debug("SNI matched {}->{}", host, _x509);
            }
            else
            {
                if (LOG.isDebugEnabled())
                    LOG.debug("SNI no match for {}", serverName);
            }

            // Return true and allow the KeyManager to accept or reject when choosing a certificate.
            // If we don't have a SNI host, or didn't see any certificate aliases,
            // just say true as it will either somehow work or fail elsewhere.
            return true;
        }

        public String getHost()
        {
            return _host;
        }

        public X509 getX509()
        {
            return _x509;
        }
    }

    public static class Client extends SslContextFactory
    {
        public Client()
        {
            this(false);
        }

        public Client(boolean trustAll)
        {
            super(trustAll);
        }

        @Override
        protected void checkConfiguration()
        {
            checkTrustAll();
            checkEndPointIdentificationAlgorithm();
            super.checkConfiguration();
        }
    }

    public static class Server extends SslContextFactory
    {
        public Server()
        {
            setEndpointIdentificationAlgorithm(null);
        }

        @Override
        public boolean getWantClientAuth()
        {
            return super.getWantClientAuth();
        }

        public void setWantClientAuth(boolean wantClientAuth)
        {
            super.setWantClientAuth(wantClientAuth);
        }

        @Override
        public boolean getNeedClientAuth()
        {
            return super.getNeedClientAuth();
        }

        @Override
        public void setNeedClientAuth(boolean needClientAuth)
        {
            super.setNeedClientAuth(needClientAuth);
        }
    }
}
