/**
 * 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.activemq;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.KeyStore;
import java.security.SecureRandom;

import javax.jms.JMSException;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.apache.activemq.broker.SslContext;
import org.apache.activemq.transport.Transport;
import org.apache.activemq.util.JMSExceptionSupport;

/**
 * An ActiveMQConnectionFactory that allows access to the key and trust managers
 * used for SslConnections. There is no reason to use this class unless SSL is
 * being used AND the key and trust managers need to be specified from within
 * code. In fact, if the URI passed to this class does not have an "ssl" scheme,
 * this class will pass all work on to its superclass.
 *
 * There are two alternative approaches you can use to provide X.509
 * certificates for the SSL connections:
 *
 * Call <code>setTrustStore</code>, <code>setTrustStorePassword</code>,
 * <code>setKeyStore</code>, and <code>setKeyStorePassword</code>.
 *
 * Call <code>setKeyAndTrustManagers</code>.
 *
 * @author sepandm@gmail.com
 */
public class ActiveMQSslConnectionFactory extends ActiveMQConnectionFactory {

    // The key and trust managers used to initialize the used SSLContext.
    protected KeyManager[] keyManager;
    protected TrustManager[] trustManager;
    protected SecureRandom secureRandom;
    protected String trustStoreType = KeyStore.getDefaultType();
    protected String trustStore;
    protected String trustStorePassword;
    protected String keyStoreType = KeyStore.getDefaultType();
    protected String keyStore;
    protected String keyStorePassword;
    protected String keyStoreKeyPassword;

    public ActiveMQSslConnectionFactory() {
        super();
    }

    public ActiveMQSslConnectionFactory(String brokerURL) {
        super(brokerURL);
    }

    public ActiveMQSslConnectionFactory(URI brokerURL) {
        super(brokerURL);
    }

    /**
     * Sets the key and trust managers used when creating SSL connections.
     *
     * @param km
     *            The KeyManagers used.
     * @param tm
     *            The TrustManagers used.
     * @param random
     *            The SecureRandom number used.
     */
    public void setKeyAndTrustManagers(final KeyManager[] km, final TrustManager[] tm, final SecureRandom random) {
        keyManager = km;
        trustManager = tm;
        secureRandom = random;
    }

    /**
     * Overriding to make special considerations for SSL connections. If we are
     * not using SSL, the superclass's method is called. If we are using SSL, an
     * SslConnectionFactory is used and it is given the needed key and trust
     * managers.
     *
     * @author sepandm@gmail.com
     */
    @Override
    protected Transport createTransport() throws JMSException {
        SslContext existing = SslContext.getCurrentSslContext();
        try {
            if (keyStore != null || trustStore != null) {
                keyManager = createKeyManager();
                trustManager = createTrustManager();
            }
            if (keyManager != null || trustManager != null) {
                SslContext.setCurrentSslContext(new SslContext(keyManager, trustManager, secureRandom));
            }
            return super.createTransport();
        } catch (Exception e) {
            throw JMSExceptionSupport.create("Could not create Transport. Reason: " + e, e);
        } finally {
            SslContext.setCurrentSslContext(existing);
        }
    }

    protected TrustManager[] createTrustManager() throws Exception {
        TrustManager[] trustStoreManagers = null;
        KeyStore trustedCertStore = KeyStore.getInstance(getTrustStoreType());

        if (trustStore != null) {
            try(InputStream tsStream = getInputStream(trustStore)) {

                trustedCertStore.load(tsStream, trustStorePassword != null ? trustStorePassword.toCharArray() : null);
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

                tmf.init(trustedCertStore);
                trustStoreManagers = tmf.getTrustManagers();
            }
        }
        return trustStoreManagers;
    }

    protected KeyManager[] createKeyManager() throws Exception {
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        KeyStore ks = KeyStore.getInstance(getKeyStoreType());
        KeyManager[] keystoreManagers = null;
        if (keyStore != null) {
            byte[] sslCert = loadClientCredential(keyStore);

            if (sslCert != null && sslCert.length > 0) {
                try(ByteArrayInputStream bin = new ByteArrayInputStream(sslCert)) {
                    //A null password may not be allowed depending on implementation
                    //Check for null so an UnrecoverableKeyException is thrown if not supported or wrong instead of NullPointerException here
                    final char[] keyStorePass = keyStorePassword != null ? keyStorePassword.toCharArray() : null;
                    ks.load(bin, keyStorePass);
                    kmf.init(ks, keyStoreKeyPassword != null ? keyStoreKeyPassword.toCharArray() : keyStorePass);
                    keystoreManagers = kmf.getKeyManagers();
                }
            }
        }
        return keystoreManagers;
    }

    protected byte[] loadClientCredential(String fileName) throws IOException {
        if (fileName == null) {
            return null;
        }
        try(InputStream in = getInputStream(fileName);
            ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            byte[] buf = new byte[512];
            int i = in.read(buf);
            while (i > 0) {
                out.write(buf, 0, i);
                i = in.read(buf);
            }
            return out.toByteArray();
        }
    }

    protected InputStream getInputStream(String urlOrResource) throws IOException {
        try {
            File ifile = new File(urlOrResource);
            // only open the file if and only if it exists
            if (ifile.exists()) {
                return new FileInputStream(ifile);
            }
        } catch (Exception e) {
        }

        InputStream ins = null;

        try {
            URL url = new URL(urlOrResource);
            ins = url.openStream();
            if (ins != null) {
                return ins;
            }
        } catch (MalformedURLException ignore) {
        }

        // Alternatively, treat as classpath resource
        if (ins == null) {
            ins = Thread.currentThread().getContextClassLoader().getResourceAsStream(urlOrResource);
        }

        if (ins == null) {
            throw new IOException("Could not load resource: " + urlOrResource);
        }

        return ins;
    }

    public String getTrustStoreType() {
        return trustStoreType;
    }

    public void setTrustStoreType(String type) {
        trustStoreType = type;
    }

    public String getTrustStore() {
        return trustStore;
    }

    /**
     * The location of a keystore file (in <code>jks</code> format) containing
     * one or more trusted certificates.
     *
     * @param trustStore
     *            If specified with a scheme, treat as a URL, otherwise treat as
     *            a classpath resource.
     */
    public void setTrustStore(String trustStore) throws Exception {
        this.trustStore = trustStore;
        trustManager = null;
    }

    public String getTrustStorePassword() {
        return trustStorePassword;
    }

    /**
     * The password to match the trust store specified by {@link setTrustStore}.
     *
     * @param trustStorePassword
     *            The password used to unlock the keystore file.
     */
    public void setTrustStorePassword(String trustStorePassword) {
        this.trustStorePassword = trustStorePassword;
    }

    public String getKeyStoreType() {
        return keyStoreType;
    }

    public void setKeyStoreType(String type) {
        keyStoreType = type;
    }


    public String getKeyStore() {
        return keyStore;
    }

    /**
     * The location of a keystore file (in <code>jks</code> format) containing a
     * certificate and its private key.
     *
     * @param keyStore
     *            If specified with a scheme, treat as a URL, otherwise treat as
     *            a classpath resource.
     */
    public void setKeyStore(String keyStore) throws Exception {
        this.keyStore = keyStore;
        keyManager = null;
    }

    public String getKeyStorePassword() {
        return keyStorePassword;
    }

    /**
     * The password to match the key store specified by {@link setKeyStore}.
     *
     * @param keyStorePassword
     *            The password, which is used both to unlock the keystore file
     *            and as the pass phrase for the private key stored in the
     *            keystore.
     */
    public void setKeyStorePassword(String keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }


    public String getKeyStoreKeyPassword() {
        return keyStoreKeyPassword;
    }

    /**
     * The password to match the key from the keyStore.
     *
     * @param keyStoreKeyPassword
     *            The password for the private key stored in the
     *            keyStore if different from keyStorePassword.
     */
    public void setKeyStoreKeyPassword(String keyStoreKeyPassword) {
        this.keyStoreKeyPassword = keyStoreKeyPassword;
    }

}
