/*
 *  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.tomcat.util.net;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.DomainLoadStoreParameter;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.file.ConfigFileLoader;
import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification;
import org.apache.tomcat.util.res.StringManager;

/**
 * Common base class for {@link SSLUtil} implementations.
 */
public abstract class SSLUtilBase implements SSLUtil {

    private static final Log log = LogFactory.getLog(SSLUtilBase.class);
    private static final StringManager sm = StringManager.getManager(SSLUtilBase.class);

    protected final SSLHostConfigCertificate certificate;

    private final String[] enabledProtocols;
    private final String[] enabledCiphers;


    protected SSLUtilBase(SSLHostConfigCertificate certificate) {
        this(certificate, true);
    }


    protected SSLUtilBase(SSLHostConfigCertificate certificate, boolean warnTls13) {
        this.certificate = certificate;
        SSLHostConfig sslHostConfig = certificate.getSSLHostConfig();

        // Calculate the enabled protocols
        Set<String> configuredProtocols = sslHostConfig.getProtocols();
        if (!isTls13Available() &&
                !sslHostConfig.isExplicitlyRequestedProtocol(Constants.SSL_PROTO_TLSv1_3)) {
            // TLS 1.3 not implemented and not explicitly requested so ignore it
            // if present
            configuredProtocols.remove(Constants.SSL_PROTO_TLSv1_3);
        }
        Set<String> implementedProtocols = getImplementedProtocols();
        List<String> enabledProtocols =
                getEnabled("protocols", getLog(), warnTls13, configuredProtocols, implementedProtocols);
        if (enabledProtocols.contains("SSLv3")) {
            log.warn(sm.getString("jsse.ssl3"));
        }
        this.enabledProtocols = enabledProtocols.toArray(new String[enabledProtocols.size()]);

        if (enabledProtocols.contains(Constants.SSL_PROTO_TLSv1_3) &&
                (sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL ||
                        sslHostConfig.getCertificateVerification() == CertificateVerification.OPTIONAL) &&
                !isTls13RenegAuthAvailable() && warnTls13) {
            log.warn(sm.getString("jsse.tls13.auth"));
        }

        // Calculate the enabled ciphers
        List<String> configuredCiphers = sslHostConfig.getJsseCipherNames();
        Set<String> implementedCiphers = getImplementedCiphers();
        List<String> enabledCiphers =
                getEnabled("ciphers", getLog(), false, configuredCiphers, implementedCiphers);
        this.enabledCiphers = enabledCiphers.toArray(new String[enabledCiphers.size()]);
    }


    static <T> List<T> getEnabled(String name, Log log, boolean warnOnSkip, Collection<T> configured,
            Collection<T> implemented) {

        List<T> enabled = new ArrayList<>();

        if (implemented.size() == 0) {
            // Unable to determine the list of available protocols. This will
            // have been logged previously.
            // Use the configuredProtocols and hope they work. If not, an error
            // will be generated when the list is used. Not ideal but no more
            // can be done at this point.
            enabled.addAll(configured);
        } else {
            enabled.addAll(configured);
            enabled.retainAll(implemented);

            if (enabled.isEmpty()) {
                // Don't use the defaults in this case. They may be less secure
                // than the configuration the user intended.
                // Force the failure of the connector
                throw new IllegalArgumentException(
                        sm.getString("sslUtilBase.noneSupported", name, configured));
            }
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("sslUtilBase.active", name, enabled));
            }
            if (log.isDebugEnabled() || warnOnSkip) {
                if (enabled.size() != configured.size()) {
                    List<T> skipped = new ArrayList<>();
                    skipped.addAll(configured);
                    skipped.removeAll(enabled);
                    String msg = sm.getString("sslUtilBase.skipped", name, skipped);
                    if (warnOnSkip) {
                        log.warn(msg);
                    } else {
                        log.debug(msg);
                    }
                }
            }
        }

        return enabled;
    }


    /*
     * Gets the key- or truststore with the specified type, path, and password.
     */
    static KeyStore getStore(String type, String provider, String path,
            String pass) throws IOException {

        KeyStore ks = null;
        InputStream istream = null;
        try {
            if (provider == null) {
                ks = KeyStore.getInstance(type);
            } else {
                ks = KeyStore.getInstance(type, provider);
            }
            if ("DKS".equalsIgnoreCase(type)) {
                URI uri = ConfigFileLoader.getURI(path);
                ks.load(new DomainLoadStoreParameter(uri, Collections.emptyMap()));
            } else {
                // Some key store types (e.g. hardware) expect the InputStream
                // to be null
                if(!("PKCS11".equalsIgnoreCase(type) ||
                        "".equalsIgnoreCase(path)) ||
                        "NONE".equalsIgnoreCase(path)) {
                    istream = ConfigFileLoader.getInputStream(path);
                }

                // The digester cannot differentiate between null and "".
                // Unfortunately, some key stores behave differently with null
                // and "".
                // JKS key stores treat null and "" interchangeably.
                // PKCS12 key stores (Java 8 onwards) don't return the cert if
                // null is used.
                // Key stores that do not use passwords expect null
                // Therefore:
                // - generally use null if pass is null or ""
                // - for JKS or PKCS12 only use null if pass is null
                //   (because JKS will auto-switch to PKCS12)
                char[] storePass = null;
                if (pass != null && (!"".equals(pass) ||
                        "JKS".equalsIgnoreCase(type) || "PKCS12".equalsIgnoreCase(type))) {
                    storePass = pass.toCharArray();
                }
                ks.load(istream, storePass);
            }
        } catch (FileNotFoundException fnfe) {
            throw fnfe;
        } catch (IOException ioe) {
            // May be expected when working with a trust store
            // Re-throw. Caller will catch and log as required
            throw ioe;
        } catch(Exception ex) {
            String msg = sm.getString("jsse.keystore_load_failed", type, path,
                    ex.getMessage());
            log.error(msg, ex);
            throw new IOException(msg);
        } finally {
            if (istream != null) {
                try {
                    istream.close();
                } catch (IOException ioe) {
                    // Do nothing
                }
            }
        }

        return ks;
    }


    @Override
    public String[] getEnabledProtocols() {
        return enabledProtocols;
    }

    @Override
    public String[] getEnabledCiphers() {
        return enabledCiphers;
    }

    protected abstract Set<String> getImplementedProtocols();
    protected abstract Set<String> getImplementedCiphers();
    protected abstract Log getLog();
    protected abstract boolean isTls13Available();
    protected abstract boolean isTls13RenegAuthAvailable();
}
