/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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
 *
 *      http://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 org.apache.camel.support.jsse;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.net.ssl.KeyManager;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLContextSpi;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

import org.apache.camel.support.jsse.FilterParameters.Patterns;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents configuration options that can be applied in the client-side or server-side context depending on what they
 * are applied to.
 */
public abstract class BaseSSLContextParameters extends JsseParameters {

    protected static final List<String> DEFAULT_CIPHER_SUITES_FILTER_INCLUDE
            = Collections.unmodifiableList(Arrays.asList(".*"));

    protected static final List<String> DEFAULT_CIPHER_SUITES_FILTER_EXCLUDE = Collections
            .unmodifiableList(Arrays.asList(".*_NULL_.*", ".*_anon_.*", ".*_EXPORT_.*", ".*_DES_.*", ".*MD5", ".*RC4.*"));

    protected static final List<String> DEFAULT_SECURE_SOCKET_PROTOCOLS_FILTER_INCLUDE
            = Collections.unmodifiableList(Arrays.asList(".*"));

    protected static final List<String> DEFAULT_SECURE_SOCKET_PROTOCOLS_FILTER_EXCLUDE
            = Collections.unmodifiableList(Arrays.asList("SSL.*"));

    private static final Logger LOG = LoggerFactory.getLogger(BaseSSLContextParameters.class);

    private static final String LS = System.lineSeparator();

    private static final String SSL_ENGINE_CIPHER_SUITE_LOG_MSG = createCipherSuiteLogMessage("SSLEngine");

    private static final String SSL_SOCKET_CIPHER_SUITE_LOG_MSG = createCipherSuiteLogMessage("SSLSocket");

    private static final String SSL_SERVER_SOCKET_CIPHER_SUITE_LOG_MSG = createCipherSuiteLogMessage("SSLServerSocket");

    private static final String SSL_ENGINE_PROTOCOL_LOG_MSG = createProtocolLogMessage("SSLEngine");

    private static final String SSL_SOCKET_PROTOCOL_LOG_MSG = createProtocolLogMessage("SSLSocket");

    private static final String SSL_SERVER_SOCKET_PROTOCOL_LOG_MSG = createProtocolLogMessage("SSLServerSocket");

    /**
     * The optional explicitly configured cipher suites for this configuration.
     */
    private CipherSuitesParameters cipherSuites;

    /**
     * The optional cipher suite filter configuration for this configuration.
     */
    private FilterParameters cipherSuitesFilter;

    /**
     * The optional explicitly configured secure socket protocol names for this configuration.
     */
    private SecureSocketProtocolsParameters secureSocketProtocols;

    /**
     * The option secure socket protocol name filter configuration for this configuration.
     */
    private FilterParameters secureSocketProtocolsFilter;

    /**
     * The optional {@link SSLSessionContext} timeout time for {@link javax.net.ssl.SSLSession}s in seconds.
     */
    private String sessionTimeout;

    protected List<SNIServerName> getSNIHostNames() {
        return Collections.emptyList();
    }

    /**
     * Returns the optional explicitly configured cipher suites for this configuration. These options are used in the
     * configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the
     * context in which they are applied.
     * <p/>
     * These values override any filters supplied in {@link #setCipherSuitesFilter(FilterParameters)}
     */
    public CipherSuitesParameters getCipherSuites() {
        return cipherSuites;
    }

    /**
     * Sets the optional explicitly configured cipher suites for this configuration. These options are used in the
     * configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the
     * context in which they are applied.
     * <p/>
     * These values override any filters supplied in {@link #setCipherSuitesFilter(FilterParameters)}
     * 
     * @param cipherSuites the suite configuration
     */
    public void setCipherSuites(CipherSuitesParameters cipherSuites) {
        this.cipherSuites = cipherSuites;
    }

    /**
     * Returns the optional cipher suite filter for this configuration. These options are used in the configuration of
     * {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the context in which
     * they are applied.
     * <p/>
     * These values are ignored if {@link #setCipherSuites(CipherSuitesParameters)} is called with a non {@code null}
     * argument.
     */
    public FilterParameters getCipherSuitesFilter() {
        return cipherSuitesFilter;
    }

    /**
     * Sets the optional cipher suite filter for this JSSE configuration. These options are used in the configuration of
     * {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the context in which
     * they are applied.
     * <p/>
     * These values are ignored if {@link #setCipherSuites(CipherSuitesParameters)} is called with a non {@code null}
     * argument.
     * 
     * @param cipherSuitesFilter the filter configuration
     */
    public void setCipherSuitesFilter(FilterParameters cipherSuitesFilter) {
        this.cipherSuitesFilter = cipherSuitesFilter;
    }

    /**
     * Returns the explicitly configured secure socket protocol names for this configuration. These options are used in
     * the configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on
     * the context in which they are applied.
     * <p/>
     * These values override any filters supplied in {@link #setSecureSocketProtocolsFilter(FilterParameters)}
     */
    public SecureSocketProtocolsParameters getSecureSocketProtocols() {
        return secureSocketProtocols;
    }

    /**
     * Sets the explicitly configured secure socket protocol names for this configuration. These options are used in the
     * configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the
     * context in which they are applied.
     * <p/>
     * These values override any filters supplied in {@link #setSecureSocketProtocolsFilter(FilterParameters)}
     */
    public void setSecureSocketProtocols(SecureSocketProtocolsParameters secureSocketProtocols) {
        this.secureSocketProtocols = secureSocketProtocols;
    }

    /**
     * Returns the optional secure socket protocol filter for this configuration. These options are used in the
     * configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the
     * context in which they are applied.
     * <p/>
     * These values are ignored if {@link #setSecureSocketProtocols(SecureSocketProtocolsParameters)} is called with a
     * non-{@code null} argument.
     */
    public FilterParameters getSecureSocketProtocolsFilter() {
        return secureSocketProtocolsFilter;
    }

    /**
     * Sets the optional secure socket protocol filter for this JSSE configuration. These options are used in the
     * configuration of {@link SSLEngine}, {@link SSLSocketFactory} and {@link SSLServerSocketFactory} depending on the
     * context in which they are applied.
     * <p/>
     * These values are ignored if {@link #setSecureSocketProtocols(SecureSocketProtocolsParameters)} is called with a
     * non-{@code null} argument.
     * 
     * @param secureSocketProtocolsFilter the filter configuration
     */
    public void setSecureSocketProtocolsFilter(FilterParameters secureSocketProtocolsFilter) {
        this.secureSocketProtocolsFilter = secureSocketProtocolsFilter;
    }

    /**
     * Returns the optional {@link SSLSessionContext} timeout time for {@link javax.net.ssl.SSLSession}s in seconds.
     */
    public String getSessionTimeout() {
        return sessionTimeout;
    }

    /**
     * Sets the optional {@link SSLSessionContext} timeout time for {@link javax.net.ssl.SSLSession}s in seconds.
     *
     * @param sessionTimeout the timeout value or {@code null} to use the default
     */
    public void setSessionTimeout(String sessionTimeout) {
        this.sessionTimeout = sessionTimeout;
    }

    /**
     * Returns a flag indicating if default values should be applied in the event that no other property of the instance
     * configures a particular aspect of the entity produced by the instance. This flag is used to allow instances of
     * this class to produce a configurer that simply passes through the current configuration of a configured entity
     * when the instance of this class would otherwise only apply some default configuration.
     *
     * @see SSLContextClientParameters
     * @see SSLContextServerParameters
     */
    protected boolean getAllowPassthrough() {
        return false;
    }

    /**
     * Configures the actual {@link SSLContext} itself with direct setter calls. This method differs from configuration
     * options that are handled by a configurer instance in that the options are part of the context itself and are not
     * part of some factory or instance object returned by the context.
     * 
     * @param  context                  the context to configure
     *
     * @throws GeneralSecurityException if there is an error configuring the context
     */
    protected void configureSSLContext(SSLContext context) throws GeneralSecurityException {
        LOG.trace("Configuring client and server side SSLContext parameters on SSLContext [{}]...", context);

        if (this.getSessionTimeout() != null) {
            LOG.debug("Configuring client and server side SSLContext session timeout on SSLContext [{}] to [{}]",
                    context, this.getSessionTimeout());
            this.configureSessionContext(context.getClientSessionContext(), this.getSessionTimeout());
            this.configureSessionContext(context.getServerSessionContext(), this.getSessionTimeout());
        }

        LOG.trace("Configured client and server side SSLContext parameters on SSLContext [{}].", context);
    }

    protected FilterParameters getDefaultCipherSuitesFilter() {
        FilterParameters filter = new FilterParameters();

        filter.getInclude().addAll(DEFAULT_CIPHER_SUITES_FILTER_INCLUDE);
        filter.getExclude().addAll(DEFAULT_CIPHER_SUITES_FILTER_EXCLUDE);

        return filter;
    }

    protected FilterParameters getDefaultSecureSocketProcotolFilter() {
        FilterParameters filter = new FilterParameters();

        filter.getInclude().addAll(DEFAULT_SECURE_SOCKET_PROTOCOLS_FILTER_INCLUDE);
        filter.getExclude().addAll(DEFAULT_SECURE_SOCKET_PROTOCOLS_FILTER_EXCLUDE);

        return filter;
    }

    /**
     * Returns the list of configurers to apply to an {@link SSLEngine} in order to fully configure it in compliance
     * with the provided configuration options. The configurers are to be applied in the order in which they appear in
     * the list.
     *
     * @param  context the context that serves as the factory for {@code SSLEngine} instances
     * 
     * @return         the needed configurers
     */
    protected List<Configurer<SSLEngine>> getSSLEngineConfigurers(SSLContext context) {

        final List<String> enabledCipherSuites = this.getCipherSuites() == null
                ? null : this.parsePropertyValues(this.getCipherSuites().getCipherSuite());

        final Patterns enabledCipherSuitePatterns;
        final Patterns defaultEnabledCipherSuitePatterns = this.getDefaultCipherSuitesFilter().getPatterns();

        if (this.getCipherSuitesFilter() != null) {
            enabledCipherSuitePatterns = this.getCipherSuitesFilter().getPatterns();
        } else {
            enabledCipherSuitePatterns = null;
        }

        ///

        final List<String> enabledSecureSocketProtocols = this.getSecureSocketProtocols() == null
                ? null : this.parsePropertyValues(this.getSecureSocketProtocols().getSecureSocketProtocol());

        final Patterns enabledSecureSocketProtocolsPatterns;
        final Patterns defaultEnabledSecureSocketProtocolsPatterns = this.getDefaultSecureSocketProcotolFilter().getPatterns();

        if (this.getSecureSocketProtocolsFilter() != null) {
            enabledSecureSocketProtocolsPatterns = this.getSecureSocketProtocolsFilter().getPatterns();
        } else {
            enabledSecureSocketProtocolsPatterns = null;
        }

        //

        final boolean allowPassthrough = getAllowPassthrough();

        //////

        Configurer<SSLEngine> sslEngineConfigurer = new Configurer<SSLEngine>() {

            @Override
            public SSLEngine configure(SSLEngine engine) {

                Collection<String> filteredCipherSuites = BaseSSLContextParameters.this
                        .filter(enabledCipherSuites, Arrays.asList(engine.getSSLParameters().getCipherSuites()),
                                Arrays.asList(engine.getEnabledCipherSuites()),
                                enabledCipherSuitePatterns, defaultEnabledCipherSuitePatterns,
                                !allowPassthrough);

                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_ENGINE_CIPHER_SUITE_LOG_MSG,
                            engine,
                            enabledCipherSuites,
                            enabledCipherSuitePatterns,
                            engine.getSSLParameters().getCipherSuites(),
                            engine.getEnabledCipherSuites(),
                            defaultEnabledCipherSuitePatterns,
                            filteredCipherSuites);
                }

                engine.setEnabledCipherSuites(filteredCipherSuites.toArray(new String[filteredCipherSuites.size()]));

                Collection<String> filteredSecureSocketProtocols = BaseSSLContextParameters.this
                        .filter(enabledSecureSocketProtocols, Arrays.asList(engine.getSSLParameters().getProtocols()),
                                Arrays.asList(engine.getEnabledProtocols()),
                                enabledSecureSocketProtocolsPatterns, defaultEnabledSecureSocketProtocolsPatterns,
                                !allowPassthrough);

                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_ENGINE_PROTOCOL_LOG_MSG,
                            engine,
                            enabledSecureSocketProtocols,
                            enabledSecureSocketProtocolsPatterns,
                            engine.getSSLParameters().getProtocols(),
                            engine.getEnabledProtocols(),
                            defaultEnabledSecureSocketProtocolsPatterns,
                            filteredSecureSocketProtocols);
                }

                engine.setEnabledProtocols(
                        filteredSecureSocketProtocols.toArray(new String[filteredSecureSocketProtocols.size()]));

                return engine;
            }
        };

        List<Configurer<SSLEngine>> sslEngineConfigurers = new LinkedList<>();
        sslEngineConfigurers.add(sslEngineConfigurer);

        return sslEngineConfigurers;
    }

    /**
     * Returns the list of configurers to apply to an {@link SSLSocketFactory} in order to fully configure it in
     * compliance with the provided configuration options. The configurers are to be applied in the order in which they
     * appear in the list.
     * <p/>
     * It is preferred to use {@link #getSSLSocketFactorySSLSocketConfigurers(SSLContext)} instead of this method as
     * {@code SSLSocketFactory} does not contain any configuration options that are non-proprietary.
     *
     * @param  context the context that serves as the factory for {@code SSLSocketFactory} instances
     * 
     * @return         the needed configurers
     * 
     * @see            #getSSLSocketFactorySSLSocketConfigurers(SSLContext)
     */
    protected List<Configurer<SSLSocketFactory>> getSSLSocketFactoryConfigurers(SSLContext context) {

        final List<Configurer<SSLSocket>> sslSocketConfigurers = this.getSSLSocketFactorySSLSocketConfigurers(context);

        Configurer<SSLSocketFactory> sslSocketFactoryConfigurer = new Configurer<SSLSocketFactory>() {

            @Override
            public SSLSocketFactory configure(SSLSocketFactory factory) {
                return new SSLSocketFactoryDecorator(
                        factory,
                        sslSocketConfigurers);
            }
        };

        List<Configurer<SSLSocketFactory>> sslSocketFactoryConfigurers = new LinkedList<>();
        sslSocketFactoryConfigurers.add(sslSocketFactoryConfigurer);

        return sslSocketFactoryConfigurers;
    }

    /**
     * Returns the list of configurers to apply to an {@link SSLServerSocketFactory} in order to fully configure it in
     * compliance with the provided configuration options. The configurers are to be applied in the order in which they
     * appear in the list.
     * <p/>
     * It is preferred to use {@link #getSSLServerSocketFactorySSLServerSocketConfigurers(SSLContext)} instead of this
     * method as {@code SSLServerSocketFactory} does not contain any configuration options that are non-proprietary.
     *
     * @param  context the context that serves as the factory for {@code SSLServerSocketFactory} instances
     * 
     * @return         the needed configurers
     * 
     * @see            #getSSLServerSocketFactorySSLServerSocketConfigurers(SSLContext)
     */
    protected List<Configurer<SSLServerSocketFactory>> getSSLServerSocketFactoryConfigurers(SSLContext context) {

        final List<Configurer<SSLServerSocket>> sslServerSocketConfigurers
                = this.getSSLServerSocketFactorySSLServerSocketConfigurers(context);

        Configurer<SSLServerSocketFactory> sslServerSocketFactoryConfigurer = new Configurer<SSLServerSocketFactory>() {

            @Override
            public SSLServerSocketFactory configure(SSLServerSocketFactory factory) {
                return new SSLServerSocketFactoryDecorator(
                        factory,
                        sslServerSocketConfigurers);
            }
        };

        List<Configurer<SSLServerSocketFactory>> sslServerSocketFactoryConfigurers = new LinkedList<>();
        sslServerSocketFactoryConfigurers.add(sslServerSocketFactoryConfigurer);

        return sslServerSocketFactoryConfigurers;
    }

    /**
     * Returns the list of configurers to apply to an {@link SSLSocket} in order to fully configure it in compliance
     * with the provided configuration options. These configurers are intended for sockets produced by a
     * {@link SSLSocketFactory}, see {@link #getSSLServerSocketFactorySSLServerSocketConfigurers(SSLContext)} for
     * configurers related to sockets produced by a {@link SSLServerSocketFactory}. The configurers are to be applied in
     * the order in which they appear in the list.
     * 
     * @param  context the context that serves as the factory for {@code SSLSocketFactory} instances
     *
     * @return         the needed configurers
     */
    protected List<Configurer<SSLSocket>> getSSLSocketFactorySSLSocketConfigurers(SSLContext context) {
        final List<String> enabledCipherSuites = this.getCipherSuites() == null
                ? null : this.parsePropertyValues(this.getCipherSuites().getCipherSuite());

        final Patterns enabledCipherSuitePatterns;
        final Patterns defaultEnabledCipherSuitePatterns = this.getDefaultCipherSuitesFilter().getPatterns();

        if (this.getCipherSuitesFilter() != null) {
            enabledCipherSuitePatterns = this.getCipherSuitesFilter().getPatterns();
        } else {
            enabledCipherSuitePatterns = null;
        }

        ///

        final List<String> enabledSecureSocketProtocols = this.getSecureSocketProtocols() == null
                ? null : this.parsePropertyValues(this.getSecureSocketProtocols().getSecureSocketProtocol());

        final Patterns enabledSecureSocketProtocolsPatterns;
        final Patterns defaultEnabledSecureSocketProtocolsPatterns = this.getDefaultSecureSocketProcotolFilter().getPatterns();

        if (this.getSecureSocketProtocolsFilter() != null) {
            enabledSecureSocketProtocolsPatterns = this.getSecureSocketProtocolsFilter().getPatterns();
        } else {
            enabledSecureSocketProtocolsPatterns = null;
        }

        //

        final boolean allowPassthrough = getAllowPassthrough();

        //////

        Configurer<SSLSocket> sslSocketConfigurer = new Configurer<SSLSocket>() {

            @Override
            public SSLSocket configure(SSLSocket socket) {

                if (!getSNIHostNames().isEmpty()) {
                    SSLParameters sslParameters = socket.getSSLParameters();
                    sslParameters.setServerNames(getSNIHostNames());
                    socket.setSSLParameters(sslParameters);
                }

                Collection<String> filteredCipherSuites = BaseSSLContextParameters.this
                        .filter(enabledCipherSuites, Arrays.asList(socket.getSSLParameters().getCipherSuites()),
                                Arrays.asList(socket.getEnabledCipherSuites()),
                                enabledCipherSuitePatterns, defaultEnabledCipherSuitePatterns,
                                !allowPassthrough);
                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_SOCKET_CIPHER_SUITE_LOG_MSG,
                            socket,
                            enabledCipherSuites,
                            enabledCipherSuitePatterns,
                            socket.getSSLParameters().getCipherSuites(),
                            socket.getEnabledCipherSuites(),
                            defaultEnabledCipherSuitePatterns,
                            filteredCipherSuites);
                }

                socket.setEnabledCipherSuites(filteredCipherSuites.toArray(new String[filteredCipherSuites.size()]));

                Collection<String> filteredSecureSocketProtocols = BaseSSLContextParameters.this
                        .filter(enabledSecureSocketProtocols, Arrays.asList(socket.getSSLParameters().getProtocols()),
                                Arrays.asList(socket.getEnabledProtocols()),
                                enabledSecureSocketProtocolsPatterns, defaultEnabledSecureSocketProtocolsPatterns,
                                !allowPassthrough);

                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_SOCKET_PROTOCOL_LOG_MSG,
                            socket,
                            enabledSecureSocketProtocols,
                            enabledSecureSocketProtocolsPatterns,
                            socket.getSSLParameters().getProtocols(),
                            socket.getEnabledProtocols(),
                            defaultEnabledSecureSocketProtocolsPatterns,
                            filteredSecureSocketProtocols);
                }

                socket.setEnabledProtocols(
                        filteredSecureSocketProtocols.toArray(new String[filteredSecureSocketProtocols.size()]));
                return socket;
            }
        };

        List<Configurer<SSLSocket>> sslSocketConfigurers = new LinkedList<>();
        sslSocketConfigurers.add(sslSocketConfigurer);

        return sslSocketConfigurers;
    }

    /**
     * Returns the list of configurers to apply to an {@link SSLServerSocket} in order to fully configure it in
     * compliance with the provided configuration options. These configurers are intended for sockets produced by a
     * {@link SSLServerSocketFactory}, see {@link #getSSLSocketFactorySSLSocketConfigurers(SSLContext)} for configurers
     * related to sockets produced by a {@link SSLSocketFactory}. The configurers are to be applied in the order in
     * which they appear in the list.
     * 
     * @param  context the context that serves as the factory for {@code SSLServerSocketFactory} instances
     * @return         the needed configurers
     */
    protected List<Configurer<SSLServerSocket>> getSSLServerSocketFactorySSLServerSocketConfigurers(SSLContext context) {
        final List<String> enabledCipherSuites = this.getCipherSuites() == null
                ? null : this.parsePropertyValues(this.getCipherSuites().getCipherSuite());

        final Patterns enabledCipherSuitePatterns;
        final Patterns defaultEnabledCipherSuitePatterns = this.getDefaultCipherSuitesFilter().getPatterns();

        if (this.getCipherSuitesFilter() != null) {
            enabledCipherSuitePatterns = this.getCipherSuitesFilter().getPatterns();
        } else {
            enabledCipherSuitePatterns = null;
        }

        ///

        final List<String> enabledSecureSocketProtocols = this.getSecureSocketProtocols() == null
                ? null : this.parsePropertyValues(this.getSecureSocketProtocols().getSecureSocketProtocol());

        final Patterns enabledSecureSocketProtocolsPatterns;
        final Patterns defaultEnabledSecureSocketProtocolsPatterns = this.getDefaultSecureSocketProcotolFilter().getPatterns();

        if (this.getSecureSocketProtocolsFilter() != null) {
            enabledSecureSocketProtocolsPatterns = this.getSecureSocketProtocolsFilter().getPatterns();
        } else {
            enabledSecureSocketProtocolsPatterns = null;
        }

        //

        final boolean allowPassthrough = getAllowPassthrough();

        //////

        Configurer<SSLServerSocket> sslServerSocketConfigurer = new Configurer<SSLServerSocket>() {

            @Override
            public SSLServerSocket configure(SSLServerSocket socket) {

                Collection<String> filteredCipherSuites = BaseSSLContextParameters.this
                        .filter(enabledCipherSuites, Arrays.asList(socket.getSupportedCipherSuites()),
                                Arrays.asList(socket.getEnabledCipherSuites()),
                                enabledCipherSuitePatterns, defaultEnabledCipherSuitePatterns,
                                !allowPassthrough);

                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_SERVER_SOCKET_CIPHER_SUITE_LOG_MSG,
                            socket,
                            enabledCipherSuites,
                            enabledCipherSuitePatterns,
                            socket.getSupportedCipherSuites(),
                            socket.getEnabledCipherSuites(),
                            defaultEnabledCipherSuitePatterns,
                            filteredCipherSuites);
                }

                socket.setEnabledCipherSuites(filteredCipherSuites.toArray(new String[filteredCipherSuites.size()]));

                Collection<String> filteredSecureSocketProtocols = BaseSSLContextParameters.this
                        .filter(enabledSecureSocketProtocols, Arrays.asList(socket.getSupportedProtocols()),
                                Arrays.asList(socket.getEnabledProtocols()),
                                enabledSecureSocketProtocolsPatterns, defaultEnabledSecureSocketProtocolsPatterns,
                                !allowPassthrough);

                if (LOG.isDebugEnabled()) {
                    LOG.debug(SSL_SERVER_SOCKET_PROTOCOL_LOG_MSG,
                            socket,
                            enabledSecureSocketProtocols,
                            enabledSecureSocketProtocolsPatterns,
                            socket.getSupportedProtocols(),
                            socket.getEnabledProtocols(),
                            defaultEnabledSecureSocketProtocolsPatterns,
                            filteredSecureSocketProtocols);
                }

                socket.setEnabledProtocols(
                        filteredSecureSocketProtocols.toArray(new String[filteredSecureSocketProtocols.size()]));
                return socket;
            }
        };

        List<Configurer<SSLServerSocket>> sslServerSocketConfigurers = new LinkedList<>();
        sslServerSocketConfigurers.add(sslServerSocketConfigurer);

        return sslServerSocketConfigurers;
    }

    /**
     * Configures a {@link SSLSessionContext}, client or server, with the supplied session timeout.
     *
     * @param  sessionContext           the context to configure
     * @param  sessionTimeout           the timeout time period
     * @throws GeneralSecurityException if {@code sessionContext} is {@code null}
     */
    protected void configureSessionContext(
            SSLSessionContext sessionContext, String sessionTimeout)
            throws GeneralSecurityException {

        int sessionTimeoutInt = Integer.parseInt(this.parsePropertyValue(sessionTimeout));

        if (sessionContext != null) {
            sessionContext.setSessionTimeout(sessionTimeoutInt);
        } else {
            throw new GeneralSecurityException(
                    "The SSLContext does not support SSLSessionContext, "
                                               + "but a session timeout is configured. Set sessionTimeout to null "
                                               + "to avoid this error.");
        }
    }

    /**
     * Filters the values in {@code availableValues} returning only the values that are explicitly listed in
     * {@code explicitValues} (returns them regardless of if they appear in {@code availableValues} or not) if
     * {@code explicitValues} is not {@code null} or according to the following rules:
     * <ol>
     * <li>Match the include patterns in {@code patterns} and don't match the exclude patterns in {@code patterns} if
     * patterns is not {@code null}.</li>
     * <li>Match the include patterns in {@code defaultPatterns} and don't match the exclude patterns in
     * {@code defaultPatterns} if patterns is {@code null} and {@code applyDefaults} is true.</li>
     * <li>Are provided in currentValues if if patterns is {@code null} and {@code applyDefaults} is false.</li>
     * </ol>
     * 
     * @param  explicitValues  the optional explicit values to use
     * @param  availableValues the available values to filter from
     * @param  patterns        the optional patterns to use when {@code explicitValues} is not used
     * @param  defaultPatterns the required patterns to use when {@code explicitValues} and {@code patterns} are not
     *                         used
     * @param  applyDefaults   flag indicating whether or not to apply defaults in the event that no explicit values and
     *                         no patterns apply
     * 
     * @return                 the filtered values
     *
     * @see                    #filter(Collection, Collection, List, List)
     */
    protected Collection<String> filter(
            Collection<String> explicitValues, Collection<String> availableValues,
            Collection<String> currentValues, Patterns patterns, Patterns defaultPatterns,
            boolean applyDefaults) {

        final List<Pattern> enabledIncludePatterns;
        final List<Pattern> enabledExcludePatterns;

        if (explicitValues == null && patterns == null && !applyDefaults) {
            return currentValues;
        }

        if (patterns != null) {
            enabledIncludePatterns = patterns.getIncludes();
            enabledExcludePatterns = patterns.getExcludes();
        } else {
            enabledIncludePatterns = defaultPatterns.getIncludes();
            enabledExcludePatterns = defaultPatterns.getExcludes();
        }

        return this.filter(
                explicitValues,
                availableValues,
                enabledIncludePatterns, enabledExcludePatterns);
    }

    /**
     * Filters the values in {@code availableValues} returning only the values that are explicitly listed in
     * {@code explicitValues} (returns them regardless of if they appear in {@code availableValues} or not) if
     * {@code explicitValues} is not {@code null} or as match the patterns in {@code includePatterns} and do not match
     * the patterns in {@code excludePatterns} if {@code explicitValues} is {@code null}.
     * 
     * @param  explicitValues  the optional explicit values to use
     * @param  availableValues the available values to filter from if {@code explicitValues} is {@code null}
     * @param  includePatterns the patterns to use for inclusion filtering, required if {@code explicitValues} is
     *                         {@code null}
     * @param  excludePatterns the patterns to use for exclusion filtering, required if {@code explicitValues} is
     *                         {@code null}
     *
     * @return                 the filtered values
     */
    protected Collection<String> filter(
            Collection<String> explicitValues, Collection<String> availableValues,
            List<Pattern> includePatterns, List<Pattern> excludePatterns) {
        Collection<String> returnValues;

        // Explicit list has precedence over filters, even when the list is
        // empty.
        if (explicitValues != null) {
            returnValues = new ArrayList<>(explicitValues);
        } else {
            returnValues = new LinkedList<>();

            for (String value : availableValues) {
                if (this.matchesOneOf(value, includePatterns)
                        && !this.matchesOneOf(value, excludePatterns)) {
                    returnValues.add(value);
                }
            }
        }

        return returnValues;
    }

    /**
     * Returns true if and only if the value is matched by one or more of the supplied patterns.
     *
     * @param value    the value to match
     * @param patterns the patterns to try to match against
     */
    protected boolean matchesOneOf(String value, List<Pattern> patterns) {
        boolean matches = false;

        for (Pattern pattern : patterns) {
            Matcher matcher = pattern.matcher(value);
            if (matcher.matches()) {
                matches = true;
                break;
            }
        }

        return matches;
    }

    /**
     * Configures a {@code T} based on the related configuration options.
     */
    interface Configurer<T> {

        /**
         * Configures a {@code T} based on the related configuration options. The return value from this method may be
         * {@code object} or it may be a decorated instance there of. Consequently, any subsequent actions on
         * {@code object} must be performed using the returned value.
         *
         * @param  object the object to configure
         * @return        {@code object} or a decorated instance there of
         */
        T configure(T object);
    }

    /**
     * Makes a decorated {@link SSLContext} appear as a normal {@code SSLContext}.
     */
    protected static final class SSLContextDecorator extends SSLContext {

        public SSLContextDecorator(SSLContextSpiDecorator decorator) {
            super(decorator, decorator.getDelegate().getProvider(), decorator.getDelegate().getProtocol());
            LOG.debug("SSLContextDecorator [{}] decorating SSLContext [{}].", this, decorator.getDelegate());
        }

        @Override
        public String toString() {
            return String.format("SSLContext[hash=%h, provider=%s, protocol=%s, needClientAuth=%s, "
                                 + "wantClientAuth=%s\n\tdefaultProtocols=%s\n\tdefaultCipherSuites=%s\n\tsupportedProtocols=%s\n\tsupportedCipherSuites=%s\n]",
                    hashCode(), getProvider(), getProtocol(), getDefaultSSLParameters().getNeedClientAuth(),
                    getDefaultSSLParameters().getWantClientAuth(),
                    collectionAsCommaDelimitedString(getDefaultSSLParameters().getProtocols()),
                    collectionAsCommaDelimitedString(getDefaultSSLParameters().getCipherSuites()),
                    collectionAsCommaDelimitedString(getSupportedSSLParameters().getProtocols()),
                    collectionAsCommaDelimitedString(getSupportedSSLParameters().getCipherSuites()));
        }
    }

    /**
     * Class needed to provide decoration of an existing {@link SSLContext}. Since {@code SSLContext} is an abstract
     * class and requires an instance of {@link SSLContextSpi}, this class effectively wraps an {@code SSLContext} as if
     * it were an {@code SSLContextSpi}, allowing us to achieve decoration.
     */
    protected static final class SSLContextSpiDecorator extends SSLContextSpi {

        private final SSLContext context;

        private final List<Configurer<SSLEngine>> sslEngineConfigurers;

        private final List<Configurer<SSLSocketFactory>> sslSocketFactoryConfigurers;

        private final List<Configurer<SSLServerSocketFactory>> sslServerSocketFactoryConfigurers;

        public SSLContextSpiDecorator(SSLContext context,
                                      List<Configurer<SSLEngine>> sslEngineConfigurers,
                                      List<Configurer<SSLSocketFactory>> sslSocketFactoryConfigurers,
                                      List<Configurer<SSLServerSocketFactory>> sslServerSocketFactoryConfigurers) {
            this.context = context;
            this.sslEngineConfigurers = sslEngineConfigurers;
            this.sslSocketFactoryConfigurers = sslSocketFactoryConfigurers;
            this.sslServerSocketFactoryConfigurers = sslServerSocketFactoryConfigurers;
        }

        @Override
        protected SSLEngine engineCreateSSLEngine() {
            SSLEngine engine = this.context.createSSLEngine();
            LOG.debug("SSLEngine [{}] created from SSLContext [{}].", engine, context);
            this.configureSSLEngine(engine);
            return engine;
        }

        @Override
        protected SSLEngine engineCreateSSLEngine(String peerHost, int peerPort) {
            SSLEngine engine = this.context.createSSLEngine(peerHost, peerPort);
            LOG.debug("SSLEngine [{}] created from SSLContext [{}].", engine, context);
            return this.configureSSLEngine(engine);
        }

        @Override
        protected SSLSessionContext engineGetClientSessionContext() {
            return this.context.getClientSessionContext();
        }

        @Override
        protected SSLSessionContext engineGetServerSessionContext() {
            return this.context.getServerSessionContext();
        }

        @Override
        protected SSLServerSocketFactory engineGetServerSocketFactory() {
            SSLServerSocketFactory factory = this.context.getServerSocketFactory();
            LOG.debug("SSLServerSocketFactoryEngine [{}] created from SSLContext [{}].", factory, context);
            return this.configureSSLServerSocketFactory(factory);
        }

        @Override
        protected SSLSocketFactory engineGetSocketFactory() {
            SSLSocketFactory factory = this.context.getSocketFactory();
            LOG.debug("SSLSocketFactory [{}] created from SSLContext [{}].", factory, context);
            return this.configureSSLSocketFactory(factory);
        }

        @Override
        protected void engineInit(
                KeyManager[] km,
                TrustManager[] tm,
                SecureRandom random)
                throws KeyManagementException {
            this.context.init(km, tm, random);
        }

        protected SSLContext getDelegate() {
            return this.context;
        }

        /**
         * Configures an {@link SSLEngine} based on the configurers in instance. The return value from this method may
         * be {@code engine} or it may be a decorated instance there of. Consequently, any subsequent actions on
         * {@code engine} must be performed using the returned value.
         * 
         * @param  engine the engine to configure
         * @return        {@code engine} or a decorated instance there of
         */
        protected SSLEngine configureSSLEngine(SSLEngine engine) {
            SSLEngine workingEngine = engine;

            for (Configurer<SSLEngine> configurer : this.sslEngineConfigurers) {
                workingEngine = configurer.configure(workingEngine);
            }

            return workingEngine;
        }

        /**
         * Configures an {@link SSLSocketFactory} based on the configurers in this instance. The return value from this
         * method may be {@code factory} or it may be a decorated instance there of. Consequently, any subsequent
         * actions on {@code factory} must be performed using the returned value.
         * 
         * @param  factory the factory to configure
         * @return         {@code factory} or a decorated instance there of
         */
        protected SSLSocketFactory configureSSLSocketFactory(SSLSocketFactory factory) {
            SSLSocketFactory workingFactory = factory;

            for (Configurer<SSLSocketFactory> configurer : this.sslSocketFactoryConfigurers) {
                workingFactory = configurer.configure(workingFactory);
            }

            return workingFactory;
        }

        /**
         * Configures an {@link SSLServerSocketFactory} based on the configurers in this instance. The return value from
         * this method may be {@code factory} or it may be a decorated instance there of. Consequently, any subsequent
         * actions on {@code factory} must be performed using the returned value.
         * 
         * @param  factory the factory to configure
         * @return         {@code factory} or a decorated instance there of
         */
        protected SSLServerSocketFactory configureSSLServerSocketFactory(
                SSLServerSocketFactory factory) {
            SSLServerSocketFactory workingFactory = factory;

            for (Configurer<SSLServerSocketFactory> configurer : this.sslServerSocketFactoryConfigurers) {
                workingFactory = configurer.configure(workingFactory);
            }

            return workingFactory;
        }
    }

    /**
     * A decorator that enables the application of configuration options to be applied to created sockets even after
     * this factory has been created and turned over to client code.
     */
    protected static final class SSLServerSocketFactoryDecorator extends SSLServerSocketFactory {

        private final SSLServerSocketFactory sslServerSocketFactory;
        private final List<Configurer<SSLServerSocket>> sslServerSocketConfigurers;

        public SSLServerSocketFactoryDecorator(SSLServerSocketFactory sslServerSocketFactory,
                                               List<Configurer<SSLServerSocket>> sslServerSocketConfigurers) {
            this.sslServerSocketFactory = sslServerSocketFactory;
            this.sslServerSocketConfigurers = sslServerSocketConfigurers;
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return this.sslServerSocketFactory.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            return this.sslServerSocketFactory.getSupportedCipherSuites();
        }

        @Override
        public ServerSocket createServerSocket() throws IOException {
            return this.configureSocket(this.sslServerSocketFactory.createServerSocket());
        }

        @Override
        public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException {
            return this.configureSocket(this.sslServerSocketFactory.createServerSocket(port, backlog, ifAddress));
        }

        @Override
        public ServerSocket createServerSocket(int port, int backlog) throws IOException {
            return this.configureSocket(this.sslServerSocketFactory.createServerSocket(port, backlog));
        }

        @Override
        public ServerSocket createServerSocket(int port) throws IOException {
            return this.configureSocket(this.sslServerSocketFactory.createServerSocket(port));
        }

        public SSLServerSocketFactory getDelegate() {
            return this.sslServerSocketFactory;
        }

        private ServerSocket configureSocket(ServerSocket s) {
            SSLServerSocket workingSocket = (SSLServerSocket) s;

            LOG.debug("Created ServerSocket [{}] from SslServerSocketFactory [{}].", s, sslServerSocketFactory);

            for (Configurer<SSLServerSocket> configurer : this.sslServerSocketConfigurers) {
                workingSocket = configurer.configure(workingSocket);
            }

            return workingSocket;
        }
    }

    /**
     * A decorator that enables the application of configuration options to be applied to created sockets even after
     * this factory has been created and turned over to client code.
     */
    protected static final class SSLSocketFactoryDecorator extends SSLSocketFactory {

        private final SSLSocketFactory sslSocketFactory;
        private final List<Configurer<SSLSocket>> sslSocketConfigurers;

        public SSLSocketFactoryDecorator(SSLSocketFactory sslSocketFactory,
                                         List<Configurer<SSLSocket>> sslSocketConfigurers) {
            this.sslSocketFactory = sslSocketFactory;
            this.sslSocketConfigurers = sslSocketConfigurers;
        }

        @Override
        public String[] getDefaultCipherSuites() {
            return sslSocketFactory.getDefaultCipherSuites();
        }

        @Override
        public String[] getSupportedCipherSuites() {
            return sslSocketFactory.getSupportedCipherSuites();
        }

        @Override
        public Socket createSocket() throws IOException {
            return configureSocket(sslSocketFactory.createSocket());
        }

        @Override
        public Socket createSocket(
                Socket s, String host,
                int port, boolean autoClose)
                throws IOException, UnknownHostException {
            return configureSocket(sslSocketFactory.createSocket(s, host, port, autoClose));
        }

        @Override
        public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
            return configureSocket(sslSocketFactory.createSocket(host, port));
        }

        @Override
        public Socket createSocket(
                String host, int port,
                InetAddress localHost, int localPort)
                throws IOException, UnknownHostException {
            return configureSocket(sslSocketFactory.createSocket(host, port, localHost, localPort));
        }

        @Override
        public Socket createSocket(InetAddress host, int port) throws IOException {
            return configureSocket(sslSocketFactory.createSocket(host, port));
        }

        @Override
        public Socket createSocket(
                InetAddress address, int port,
                InetAddress localAddress, int localPort)
                throws IOException {
            return configureSocket(sslSocketFactory.createSocket(address, port, localAddress, localPort));
        }

        public SSLSocketFactory getDelegate() {
            return this.sslSocketFactory;
        }

        private Socket configureSocket(Socket s) {
            SSLSocket workingSocket = (SSLSocket) s;

            LOG.debug("Created Socket [{}] from SocketFactory [{}].", s, sslSocketFactory);

            for (Configurer<SSLSocket> configurer : this.sslSocketConfigurers) {
                workingSocket = configurer.configure(workingSocket);
            }

            return workingSocket;
        }
    }

    private static String collectionAsCommaDelimitedString(String[] col) {
        return col == null || col.length == 0 ? "" : String.join(",", col);
    }

    private static String createCipherSuiteLogMessage(String entityName) {
        return "Configuring " + entityName + " [{}] with " + LS
               + "\t explicitly set cipher suites [{}]," + LS
               + "\t cipher suite patterns [{}]," + LS
               + "\t available cipher suites [{}]," + LS
               + "\t currently enabled cipher suites [{}]," + LS
               + "\t and default cipher suite patterns [{}]." + LS
               + "\t Resulting enabled cipher suites are [{}].";
    }

    private static String createProtocolLogMessage(String entityName) {
        return "Configuring " + entityName + " [{}] with " + LS
               + "\t explicitly set protocols [{}]," + LS
               + "\t protocol patterns [{}]," + LS
               + "\t available protocols [{}]," + LS
               + "\t currently enabled protocols [{}]," + LS
               + "\t and default protocol patterns [{}]." + LS
               + "\t Resulting enabled protocols are [{}].";
    }
}
