/*
 *  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.directory.studio.connection.core.io;


import java.security.KeyStore;
import java.security.cert.CertPathValidatorException.Reason;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import org.apache.directory.api.ldap.model.exception.LdapTlsHandshakeExceptionClassifier;
import org.apache.directory.api.ldap.model.exception.LdapTlsHandshakeFailCause;
import org.apache.directory.api.ldap.model.exception.LdapTlsHandshakeFailCause.LdapApiReason;
import org.apache.directory.studio.connection.core.ConnectionCorePlugin;
import org.apache.directory.studio.connection.core.ICertificateHandler;
import org.apache.directory.studio.connection.core.Messages;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;


/**
 * A wrapper for a real {@link TrustManager}. If the certificate chain is not trusted
 * then ask the user.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class StudioTrustManager implements X509TrustManager
{
    private X509TrustManager jvmTrustManager;
    private String host;

    /**
     * Creates a new instance of StudioTrustManager.
     * 
     * @param jvmTrustManager the JVM trust manager
     * 
     * @throws Exception the exception
     */
    public StudioTrustManager( X509TrustManager jvmTrustManager ) throws Exception
    {
        this.jvmTrustManager = jvmTrustManager;
    }


    /**
     * Sets the host, used to verify the hostname of the certificate.
     * 
     * @param host the new host
     */
    public void setHost( String host )
    {
        this.host = host;
    }


    /**
     * {@inheritDoc}
     */
    public void checkClientTrusted( X509Certificate[] chain, String authType ) throws CertificateException
    {
        jvmTrustManager.checkClientTrusted( chain, authType );
    }


    /**
     * {@inheritDoc}
     */
    public void checkServerTrusted( X509Certificate[] chain, String authType ) throws CertificateException
    {
        // check permanent trusted certificates, return on success
        try
        {
            X509TrustManager permanentTrustManager = getPermanentTrustManager();
            if ( permanentTrustManager != null )
            {
                permanentTrustManager.checkServerTrusted( chain, authType );
                return;
            }
        }
        catch ( CertificateException ce )
        {
        }

        // check temporary trusted certificates, return on success
        try
        {
            X509TrustManager sessionTrustManager = getSessionTrustManager();
            if ( sessionTrustManager != null )
            {
                sessionTrustManager.checkServerTrusted( chain, authType );
                return;
            }
        }
        catch ( CertificateException ce )
        {
        }

        // below here no manually trusted certificate (either permanent or temporary) matched
        Map<Reason, LdapTlsHandshakeFailCause> failCauses = new LinkedHashMap<>();
        CertificateException certificateException = null;

        // perform trust check of JVM trust manager
        try
        {
            jvmTrustManager.checkServerTrusted( chain, authType );
        }
        catch ( CertificateException ce )
        {
            certificateException = ce;
            LdapTlsHandshakeFailCause failCause = LdapTlsHandshakeExceptionClassifier.classify( ce, chain[0] );
            failCauses.put( failCause.getReason(), failCause );
        }

        // perform a certificate validity check
        try
        {
            chain[0].checkValidity();
        }
        catch ( CertificateException ce )
        {
            certificateException = ce;
            LdapTlsHandshakeFailCause failCause = LdapTlsHandshakeExceptionClassifier.classify( ce, chain[0] );
            failCauses.put( failCause.getReason(), failCause );
        }

        // perform host name verification
        try
        {
            DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
            hostnameVerifier.verify( host, chain[0] );
        }
        catch ( SSLException ssle )
        {
            certificateException = new CertificateException( ssle );
            LdapTlsHandshakeFailCause failCause = new LdapTlsHandshakeFailCause( ssle, ssle,
                LdapApiReason.HOST_NAME_VERIFICATION_FAILED, "Hostname verification failed" );
            failCauses.put( failCause.getReason(), failCause );
        }

        if ( !failCauses.isEmpty() )
        {
            // either trust check or host name verification
            // ask for confirmation
            ICertificateHandler ch = ConnectionCorePlugin.getDefault().getCertificateHandler();
            ICertificateHandler.TrustLevel trustLevel = ch.verifyTrustLevel( host, chain, failCauses.values() );
            switch ( trustLevel )
            {
                case Permanent:
                    ConnectionCorePlugin.getDefault().getPermanentTrustStoreManager().addCertificate( chain[0] );
                    break;
                case Session:
                    ConnectionCorePlugin.getDefault().getSessionTrustStoreManager().addCertificate( chain[0] );
                    break;
                case Not:
                    throw certificateException;
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    public X509Certificate[] getAcceptedIssuers()
    {
        return jvmTrustManager.getAcceptedIssuers();
    }


    /**
     * Gets the permanent trust manager, based on the permanent trust store.
     * 
     * @return the permanent trust manager, null if the trust store is empty
     * 
     * @throws CertificateException the certificate exception
     */
    private X509TrustManager getPermanentTrustManager() throws CertificateException
    {
        KeyStore permanentTrustStore = ConnectionCorePlugin.getDefault().getPermanentTrustStoreManager().getKeyStore();
        X509TrustManager permanentTrustManager = getTrustManager( permanentTrustStore );
        return permanentTrustManager;
    }


    /**
     * Gets the session trust manager, based on the session trust store.
     * 
     * @return the session trust manager, null if the trust store is empty
     * 
     * @throws CertificateException the certificate exception
     */
    private X509TrustManager getSessionTrustManager() throws CertificateException
    {
        KeyStore sessionTrustStore = ConnectionCorePlugin.getDefault().getSessionTrustStoreManager().getKeyStore();
        X509TrustManager sessionTrustManager = getTrustManager( sessionTrustStore );
        return sessionTrustManager;
    }


    private X509TrustManager getTrustManager( KeyStore trustStore ) throws CertificateException
    {
        try
        {
            Enumeration<String> aliases = trustStore.aliases();
            if ( aliases.hasMoreElements() )
            {
                TrustManagerFactory factory = TrustManagerFactory.getInstance( TrustManagerFactory
                    .getDefaultAlgorithm() );
                factory.init( trustStore );
                TrustManager[] permanentTrustManagers = factory.getTrustManagers();
                TrustManager permanentTrustManager = permanentTrustManagers[0];
                return ( X509TrustManager ) permanentTrustManager;
            }
        }
        catch ( Exception e )
        {
            throw new CertificateException( Messages.StudioTrustManager_CantCreateTrustManager, e );
        }

        return null;
    }

}
