# ClientSSLConfigurationBuilder.java

    /*
     *  Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
     *
     *  WSO2 Inc. 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.synapse.transport.netty.sender;

    import org.apache.axiom.om.OMElement;
    import org.apache.axis2.AxisFault;
    import org.apache.axis2.description.Parameter;
    import org.apache.axis2.description.TransportOutDescription;
    import org.apache.axis2.transport.base.ParamUtils;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.synapse.transport.netty.BridgeConstants;
    import org.apache.synapse.transport.netty.config.TargetConfiguration;
    import org.apache.synapse.transport.nhttp.util.SecureVaultValueReader;
    import org.wso2.securevault.SecretResolver;
    import org.wso2.transport.http.netty.contract.config.SslConfiguration;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Objects;
    import javax.xml.namespace.QName;

    /**
     * Builder class for the Client SSLConfiguration.
     */
    public class ClientSSLConfigurationBuilder {

        private static final Log LOG = LogFactory.getLog(ClientSSLConfigurationBuilder.class);

        private String keyStore;
        private String keyStorePass;
        private String certPass;
        private String trustStore;
        private String trustStorePass;
        private String tlsStoreType;
        private String sslProtocol = BridgeConstants.TLS_PROTOCOL;
        private boolean validateCertEnabled;
        private int cacheValidityPeriod;
        private int cacheSize;
        private int sessionTimeOut;
        private long handshakeTimeOut;
        private boolean disableCertValidation;
        private boolean hostnameVerifier = false;
        List<org.wso2.transport.http.netty.contract.config.Parameter> clientParamList = new ArrayList<>();

        /**
         * Populate the SSL configuration values in the given client SslConfiguration.
         *
         * @param sslConfiguration instance that represents the client's ssl configuration
         */
        public void setClientSSLConfig(SslConfiguration sslConfiguration) {

            sslConfiguration.setKeyStoreFile(keyStore);
            sslConfiguration.setKeyStorePass(keyStorePass);
            sslConfiguration.setCertPass(certPass);
            sslConfiguration.setTLSStoreType(tlsStoreType);
            if (disableCertValidation) {
                sslConfiguration.disableSsl();
                return;
            }
            sslConfiguration.setTrustStoreFile(trustStore);
            sslConfiguration.setTrustStorePass(trustStorePass);
            sslConfiguration.setSSLProtocol(sslProtocol);
            if (validateCertEnabled) {
                sslConfiguration.setValidateCertEnabled(true);
                if (cacheValidityPeriod > 0) {
                    sslConfiguration.setCacheValidityPeriod(cacheValidityPeriod);
                }
                if (cacheSize > 0) {
                    sslConfiguration.setCacheSize(cacheSize);
                }
            }
            sslConfiguration.setSslSessionTimeOut(sessionTimeOut);
            sslConfiguration.setSslHandshakeTimeOut(handshakeTimeOut);
            sslConfiguration.setParameters(clientParamList);
            sslConfiguration.setHostNameVerificationEnabled(hostnameVerifier);
        }

        /**
         * Parse SSL configuration from axis2.xml.
         *
         * @param targetConfiguration the configuration of the sender
         * @param transportOut        TransportOutDescription of the configuration
         */
        public ClientSSLConfigurationBuilder parseSSL(TargetConfiguration targetConfiguration,
                                                      TransportOutDescription transportOut) throws AxisFault {

            SecretResolver secretResolver = targetConfiguration.getConfigurationContext()
                    .getAxisConfiguration().getSecretResolver();

            // Populate KeyStore configs
            Parameter keyParam = transportOut.getParameter(BridgeConstants.KEY_STORE);
            populateKeyStoreConfigs(keyParam, secretResolver);

            // Populate Truststore configs
            Parameter trustParam = transportOut.getParameter(BridgeConstants.TRUST_STORE);
            boolean novalidateCert = ParamUtils.getOptionalParamBoolean(transportOut, BridgeConstants.NO_VALIDATE_CERT,
                    false);

            if (isCertValidationDisabled(novalidateCert, trustParam)) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Server certificate validation (trust) has been disabled.");
                }
                disableCertValidation = true;
                return this;
            }
            populateTrustStoreConfigs(trustParam, secretResolver, novalidateCert);

            // Populate HttpsProtocols and SSLProtocol configs
            Parameter httpsProtocolsParam =
                    transportOut.getParameter(BridgeConstants.HTTPS_PROTOCOL);
            Parameter sslProtocolParam =
                    transportOut.getParameter(BridgeConstants.SSL_PROTOCOL);
            populateProtocolConfigs(sslProtocolParam, httpsProtocolsParam);

            // Populate certificate validation configs
            Parameter cvp = transportOut.getParameter(BridgeConstants.CLIENT_REVOCATION);
            populateCertValidationConfigs(cvp);

            // Populate preferred ciphers configs
            Parameter preferredCiphersParam =
                    transportOut.getParameter(BridgeConstants.PREFERRED_CIPHERS);
            populateCiphersConfigs(preferredCiphersParam);

            // Populate session and handshake timeout configs
            Parameter sessionTimeoutParam =
                    transportOut.getParameter(BridgeConstants.SSL_SESSION_TIMEOUT);
            Parameter handshakeTimeoutParam =
                    transportOut.getParameter(BridgeConstants.SSL_HANDSHAKE_TIMEOUT);
            populateTimeoutConfigs(sessionTimeoutParam, handshakeTimeoutParam);

            //TODO: populateHostnameVerifierConfigs with Strict, AllowAll, etc.
            Parameter hostnameVerifierParam = transportOut.getParameter(BridgeConstants.HOSTNAME_VERIFIER);
            populateHostnameVerifierConfigs(hostnameVerifierParam);
            return this;
        }

        private boolean isCertValidationDisabled(boolean novalidateCert,
                                                 Parameter trustParam) {

            if (trustParam != null && trustParam.getParameterElement().getFirstElement() != null) {
                return false;
            }
            return novalidateCert;
        }

        private void populateKeyStoreConfigs(Parameter keyParam,
                                             SecretResolver secretResolver) throws AxisFault {

            OMElement keyStoreElt = null;
            if (keyParam != null) {
                keyStoreElt = keyParam.getParameterElement().getFirstElement();
            }
            if (keyStoreElt == null) {
                throw new AxisFault("KeyStore must be provided for secure connection");
            }
            OMElement keyStoreLocationElement = keyStoreElt
                    .getFirstChildWithName(new QName(BridgeConstants.STORE_LOCATION));
            OMElement tlsStoreTypeElement = keyStoreElt.getFirstChildWithName(new QName(BridgeConstants.TYPE));
            OMElement passwordElement = keyStoreElt.getFirstChildWithName(new QName(BridgeConstants.PASSWORD));
            OMElement keyPasswordElement = keyStoreElt.getFirstChildWithName(new QName(BridgeConstants.KEY_PASSWORD));

            if (Objects.nonNull(keyStoreLocationElement)) {
                keyStore = keyStoreLocationElement.getText();
            }
            if (Objects.isNull(keyStore) || keyStore.isEmpty()) {
                throw new AxisFault("KeyStore file location must be provided for secure connection");
            }
            if (Objects.nonNull(tlsStoreTypeElement)) {
                tlsStoreType = tlsStoreTypeElement.getText();
            }
            if (passwordElement == null) {
                throw new AxisFault("Cannot proceed because Password element is missing in KeyStore");
            }
            if (keyPasswordElement == null) {
                throw new AxisFault("Cannot proceed because KeyPassword element is missing in KeyStore");
            }
            keyStorePass = SecureVaultValueReader.getSecureVaultValue(secretResolver, passwordElement);
            certPass = SecureVaultValueReader.getSecureVaultValue(secretResolver, keyPasswordElement);
        }

        private void populateTrustStoreConfigs(Parameter trustParam,
                                               SecretResolver secretResolver, boolean novalidateCert) throws AxisFault {

            OMElement trustStoreElt = null;
            if (trustParam != null) {
                trustStoreElt = trustParam.getParameterElement().getFirstElement();
            }
            if (trustStoreElt == null) {
                throw new AxisFault("If server certification validation (novalidatecert) parameter is not configured to "
                        + "true, Truststore should be specified for secure connection");
            }
            if (novalidateCert) {
                if (LOG.isWarnEnabled()) {
                    LOG.warn("Ignoring novalidatecert parameter since a truststore has been specified");
                }
            }
            trustStore = trustStoreElt.getFirstChildWithName(new QName(BridgeConstants.STORE_LOCATION)).getText();
            String type = trustStoreElt.getFirstChildWithName(new QName(BridgeConstants.TYPE)).getText();
            OMElement passwordElement = trustStoreElt.getFirstChildWithName(new QName(BridgeConstants.PASSWORD));
            if (Objects.isNull(passwordElement)) {
                throw new AxisFault("Cannot proceed because Password element is missing in TrustStore");
            }
            trustStorePass = SecureVaultValueReader.getSecureVaultValue(secretResolver, passwordElement);
        }

        private void populateProtocolConfigs(Parameter sslProtocolParam,
                                             Parameter httpsProtocolsParam) {

            if (Objects.nonNull(sslProtocolParam) && !sslProtocolParam.getValue().toString().isEmpty()) {
                this.sslProtocol = sslProtocolParam.getValue().toString();
            }

            if (Objects.isNull(httpsProtocolsParam) || Objects.isNull(httpsProtocolsParam.getParameterElement())) {
                return;
            }

            String configuredHttpsProtocols = httpsProtocolsParam.getParameterElement().getText().replaceAll("\\s", "");

            if (!configuredHttpsProtocols.isEmpty()) {
                org.wso2.transport.http.netty.contract.config.Parameter serverProtocols
                        = new org.wso2.transport.http.netty.contract.config.Parameter(BridgeConstants.SSL_ENABLED_PROTOCOLS,
                        configuredHttpsProtocols);
                clientParamList.add(serverProtocols);
            }
        }

        private void populateCertValidationConfigs(Parameter cvp) {

            final String cvEnable = cvp != null ?
                    cvp.getParameterElement().getAttribute(new QName("enable")).getAttributeValue() : null;

            if (BridgeConstants.VALUE_TRUE.equalsIgnoreCase(cvEnable)) {
                validateCertEnabled = true;
                String cacheSizeString = cvp.getParameterElement().getFirstChildWithName(
                        new QName(BridgeConstants.CACHE_SIZE)).getText();
                String cacheDelayString = cvp.getParameterElement().getFirstChildWithName(
                        new QName(BridgeConstants.CACHE_DELAY)).getText();
                Integer cacheSize = null;
                Integer cacheDelay = null;
                try {
                    cacheSize = new Integer(cacheSizeString);
                    cacheDelay = new Integer(cacheDelayString);
                } catch (NumberFormatException e) {
                    // do nothing
                }

                if (Objects.nonNull(cacheDelay) && cacheDelay != 0) {
                    this.cacheValidityPeriod = Math.toIntExact(cacheDelay);
                }
                if (Objects.nonNull(cacheSize) && cacheSize != 0) {
                    this.cacheSize = Math.toIntExact(cacheSize);
                }
            }
        }

        private void populateCiphersConfigs(Parameter preferredCiphersParam) {

            if (Objects.isNull(preferredCiphersParam) || Objects.isNull(preferredCiphersParam.getParameterElement())) {
                return;
            }

            String preferredCiphers = preferredCiphersParam.getParameterElement().getText().replaceAll("\\s", "");
            if (!preferredCiphers.isEmpty()) {
                org.wso2.transport.http.netty.contract.config.Parameter serverParameters =
                        new org.wso2.transport.http.netty.contract.config.Parameter(
                                BridgeConstants.CIPHERS, preferredCiphers);
                clientParamList.add(serverParameters);
            }
        }

        private void populateTimeoutConfigs(Parameter sessionTimeoutParam,
                                            Parameter handshakeTimeoutParam) {

            if (Objects.nonNull(sessionTimeoutParam) && Objects.nonNull(sessionTimeoutParam.getParameterElement())) {
                String sessionTimeoutStr = sessionTimeoutParam.getParameterElement().getText();
                try {
                    int sessionTimeout = Integer.parseInt(sessionTimeoutStr);
                    if (sessionTimeout > 0) {
                        this.sessionTimeOut = sessionTimeout;
                    } else {
                        LOG.warn("SessionTimeout should be a valid positive number. But found : " + sessionTimeoutStr
                                + ". Hence, using the default value of 86400s/24h");
                    }
                } catch (NumberFormatException e) {
                    LOG.warn("Invalid number found for SSL SessionTimeout : " + sessionTimeoutStr
                            + ". Hence, using the default value of 86400s/24h");
                }
            }

            if (Objects.nonNull(handshakeTimeoutParam) && Objects.nonNull(handshakeTimeoutParam.getParameterElement())) {
                String handshakeTimeoutStr = handshakeTimeoutParam.getParameterElement().getText();
                try {
                    int handshakeTimeOut = Integer.parseInt(handshakeTimeoutStr);
                    if (handshakeTimeOut > 0) {
                        this.handshakeTimeOut = handshakeTimeOut;
                    } else {
                        LOG.warn("HandshakeTimeout should be a valid positive number. But found : " + handshakeTimeoutStr
                                + ". Hence, using the default value of 10s");
                    }
                } catch (NumberFormatException e) {
                    LOG.warn("Invalid number found for ssl handshakeTimeout : " + handshakeTimeoutStr +
                            ". Hence, using the default value of 10s");
                }
            }
        }

        private void populateHostnameVerifierConfigs(Parameter hostnameVerifierParam) {

            if (Objects.nonNull(hostnameVerifierParam) && Objects.nonNull(hostnameVerifierParam.getParameterElement())) {
                String hostnameVerifierStr = hostnameVerifierParam.getParameterElement().getText();
                if ("Enabled".equalsIgnoreCase(hostnameVerifierStr)) {
                    this.hostnameVerifier = true;
                }
            }
        }
    }