/*
 * 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.netbeans.modules.j2ee.weblogic9;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.swing.SwingUtilities;
import org.netbeans.api.keyring.Keyring;
import org.netbeans.modules.j2ee.deployment.plugins.api.InstanceProperties;
import org.netbeans.modules.weblogic.common.api.WebLogicConfiguration;
import org.netbeans.modules.weblogic.common.spi.WebLogicTrustHandler;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.ServiceProvider;

/**
 *
 * @author Petr Hejl
 */
@ServiceProvider(service = WebLogicTrustHandler.class)
public class WLTrustHandler implements WebLogicTrustHandler {

    public static final String TRUST_EXCEPTION_PROPERTY = "trustException"; // NOI18N

    private static final Logger LOGGER = Logger.getLogger(WLTrustHandler.class.getName());

    private static final RequestProcessor TRUST_CHECK = new RequestProcessor("Trust Handler Check", 5);

    private static final RequestProcessor TRUST_MANAGER_ACCESS = new RequestProcessor(WLTrustHandler.class);

    private static final int CHECK_TIMEOUT = 5000;

    private static final String TRUST_STORE_PATH = "J2EE/TrustStores/wlstruststore.jks"; // NOI18N

    private static final String TRUST_PASSWORD_KEY = "nb_weblogic_truststore"; // NOI18N

    private static final SecureRandom RANDOM = new SecureRandom();

    @Override
    public TrustManager getTrustManager(WebLogicConfiguration config) throws GeneralSecurityException {
        return new DelegatingTrustManager(config);
    }

    @Override
    public Map<String, String> getTrustProperties(WebLogicConfiguration config) {
        check(config);

        final InstanceProperties ip = InstanceProperties.getInstanceProperties(WLDeploymentFactory.getUrl(config));
        boolean trustException = Boolean.parseBoolean(ip.getProperty(TRUST_EXCEPTION_PROPERTY));
        if (!trustException) {
            return Collections.emptyMap();
        }
        FileObject fo = FileUtil.getConfigFile(TRUST_STORE_PATH);
        if (fo == null) {
            return Collections.emptyMap();
        }
        File file = FileUtil.toFile(fo);
        if (file == null) {
            return Collections.emptyMap();
        }

        Map<String, String> result = new HashMap<String, String>();
        result.put("weblogic.security.TrustKeyStore", "CustomTrust"); // NOI18N
        result.put("weblogic.security.CustomTrustKeyStoreType", "JKS"); // NOI18N
        result.put("weblogic.security.CustomTrustKeyStoreFileName", file.getAbsolutePath()); // NOI18N
        result.put("weblogic.security.SSL.ignoreHostnameVerification", "true"); // NOI18N
        return result;
    }

    public static synchronized void removeFromTrustStore(String url) throws GeneralSecurityException, IOException {
        FileObject root = FileUtil.getConfigRoot();
        FileObject ts = root.getFileObject(TRUST_STORE_PATH);
        if (ts == null) {
            return;
        }

        char[] password = Keyring.read(TRUST_PASSWORD_KEY);

        KeyStore keystore = KeyStore.getInstance("JKS"); // NOI18N
        InputStream is = new BufferedInputStream(ts.getInputStream());
        try {
            keystore.load(is, password);
        } catch (IOException ex) {
            LOGGER.log(Level.INFO, null, ex);
            return;
        } finally {
            is.close();
        }

        keystore.deleteEntry(url);

        OutputStream out = new BufferedOutputStream(ts.getOutputStream());
        try {
            keystore.store(out, password);
        } finally {
            out.close();
        }
    }

    public static boolean check(final WebLogicConfiguration config) {
        // we use the different thread as this check thread may have
        // the interrupted flag set as a result of normal operation on socket :(
        Future<Boolean> task = TRUST_CHECK.submit(new Callable() {

            @Override
            public Boolean call() {
                try {
                    SSLContext context = SSLContext.getInstance("TLS"); // NOI18N
                    context.init(null, new TrustManager[]{new DelegatingTrustManager(config)}, RANDOM);
                    SSLSocket socket = (SSLSocket) context.getSocketFactory().createSocket();
                    try {
                        // we just trigger the trust manager here
                        socket.connect(new InetSocketAddress(config.getHost(), config.getPort()), CHECK_TIMEOUT); // NOI18N
                        socket.setSoTimeout(CHECK_TIMEOUT);
                        PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
                        try {
                            out.println("GET / HTTP/1.1\nHost:\n"); // NOI18N
                            return true;
                        } finally {
                            out.close();
                        }
                    } finally {
                        socket.close();
                    }
                } catch (GeneralSecurityException ex) {
                    LOGGER.log(Level.WARNING, null, ex);
                } catch (IOException ex) {
                    LOGGER.log(Level.INFO, null, ex);
                }
                return false;
            }
        });

        try {
            return task.get(CHECK_TIMEOUT + 1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException ex) {
            LOGGER.log(Level.INFO, null, ex);
            Thread.currentThread().interrupt();
        } catch (ExecutionException ex) {
            LOGGER.log(Level.INFO, null, ex);
        } catch (TimeoutException ex) {
            LOGGER.log(Level.FINE, null, ex);
        }
        return false;
    }

    private static class DelegatingTrustManager implements X509TrustManager {

        private final WebLogicConfiguration config;

        /*
         * The default PKIX X509TrustManager9.  We'll delegate
         * decisions to it, and fall back to the logic in this class if the
         * default X509TrustManager doesn't trust it.
         */
        private X509TrustManager pkixTrustManager;

        DelegatingTrustManager(WebLogicConfiguration config) throws NoSuchAlgorithmException, KeyStoreException {
            this.config = config;
            // create a "default" JSSE X509TrustManager.

            TrustManagerFactory tmf
                    = TrustManagerFactory.getInstance("PKIX"); // NOI18N
            tmf.init((KeyStore) null);

            TrustManager[] tms = tmf.getTrustManagers();

            /*
             * Iterate over the returned trustmanagers, look
             * for an instance of X509TrustManager.  If found,
             * use that as our "default" trust manager.
             */
            for (int i = 0; i < tms.length; i++) {
                if (tms[i] instanceof X509TrustManager) {
                    pkixTrustManager = (X509TrustManager) tms[i];
                    break;
                }
            }
        }

        /*
         * Delegate to the default trust manager.
         */
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            throw new CertificateException("TrustManager does not trust any client");
        }

        @NbBundle.Messages({
            "LBL_Certificate=The Server Certificate Problem",
            "LBL_RemoveServer=Remove Server"
        })
        @Override
        public void checkServerTrusted(final X509Certificate[] chain, final String authType)
                throws CertificateException {
            final String url = WLDeploymentFactory.getUrl(config);
            final InstanceProperties ip = InstanceProperties.getInstanceProperties(url);
            if (ip == null) {
                return;
            }
            try {
                pkixTrustManager.checkServerTrusted(chain, authType);
                String rawTrustValue = ip.getProperty(TRUST_EXCEPTION_PROPERTY);
                if (rawTrustValue != null) {
                    ip.setProperty(TRUST_EXCEPTION_PROPERTY, null);
                    try {
                        removeFromTrustStore(url);
                    } catch (GeneralSecurityException ex) {
                        LOGGER.log(Level.INFO, null, ex);
                    } catch (IOException ex) {
                        LOGGER.log(Level.INFO, null, ex);
                    }
                }
            } catch (CertificateException excep) {
                LOGGER.log(Level.FINE, null, excep);
                Future<Boolean> task = TRUST_MANAGER_ACCESS.submit(new Callable<Boolean>() {

                    @Override
                    public Boolean call() throws GeneralSecurityException, IOException {
                        String rawTrustValue = ip.getProperty(TRUST_EXCEPTION_PROPERTY);
                        boolean trustException = Boolean.parseBoolean(rawTrustValue);
                        if (trustException) {
                            FileObject fo = FileUtil.getConfigFile(TRUST_STORE_PATH);
                            if (fo != null) {
                                try {
                                    X509TrustManager m = createTrustManager(fo);
                                    try {
                                        m.checkServerTrusted(chain, authType);
                                        return true;
                                    } catch (CertificateException ex) {
                                        LOGGER.log(Level.FINE, null, ex);
                                    }
                                } catch (GeneralSecurityException ex) {
                                    // proceed to dialog
                                    LOGGER.log(Level.INFO, null, ex);
                                } catch (IOException ex) {
                                    // proceed to dialog
                                    LOGGER.log(Level.INFO, null, ex);
                                }
                            }
                        }

                        if (rawTrustValue != null && !trustException) {
                            return false;
                        }
                        X509Certificate[] sorted = sortChain(chain);
                        X509Certificate last = sorted[sorted.length - 1];

                        String removeOption = Bundle.LBL_RemoveServer();
                        NotifyDescriptor descriptor = new NotifyDescriptor(
                                new CertificateQuestionPanel(last), Bundle.LBL_Certificate(),
                                NotifyDescriptor.YES_NO_OPTION, NotifyDescriptor.WARNING_MESSAGE,
                                new Object[]{removeOption, NotifyDescriptor.YES_OPTION,
                                    NotifyDescriptor.NO_OPTION}, NotifyDescriptor.NO_OPTION);

                        Object result = DialogDisplayer.getDefault().notify(descriptor);
                        if (result == NotifyDescriptor.YES_OPTION) {
                            addToTrustStore(url, last);
                            ip.setProperty(TRUST_EXCEPTION_PROPERTY, Boolean.TRUE.toString());
                            return true;
                        } else if (result.equals(removeOption)) {
                            ip.setProperty(TRUST_EXCEPTION_PROPERTY, Boolean.FALSE.toString());
                            SwingUtilities.invokeLater(new Runnable() {

                                @Override
                                public void run() {
                                    InstanceProperties.removeInstance(url);
                                }
                            });
                            return false;
                        } else {
                            ip.setProperty(TRUST_EXCEPTION_PROPERTY, Boolean.FALSE.toString());
                            return false;
                        }
                    }
                });
                try {
                    if (!task.get()) {
                        throw excep;
                    }
                } catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                } catch (ExecutionException ex) {
                    LOGGER.log(Level.WARNING, null, ex);
                    throw new CertificateException(ex);
                }
            }
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return pkixTrustManager.getAcceptedIssuers();
        }
    }

    private static X509Certificate[] sortChain(X509Certificate[] certificates) {
        if ((certificates != null) && (certificates.length == 1)) {
            return certificates;
        } else {
            List<X509Certificate> certs = new ArrayList<X509Certificate>();
            certs.addAll(Arrays.asList(certificates));
            X509Certificate certChain = certs.get(0);
            certs.remove(certChain);
            LinkedList<X509Certificate> chainList = new LinkedList<X509Certificate>();
            chainList.add(certChain);
            Principal certIssuer = certChain.getIssuerDN();
            Principal certSubject = certChain.getSubjectDN();
            while (!certs.isEmpty()) {
                List<X509Certificate> tempcerts = new ArrayList<X509Certificate>();
                tempcerts.addAll(certs);
                for (X509Certificate cert : tempcerts) {
                    if (cert.getIssuerDN().equals(certSubject)) {
                        chainList.addFirst(cert);
                        certSubject = cert.getSubjectDN();
                        certs.remove(cert);
                        continue;
                    }

                    if (cert.getSubjectDN().equals(certIssuer)) {
                        chainList.addLast(cert);
                        certIssuer = cert.getIssuerDN();
                        certs.remove(cert);
                    }
                }
            }
            return chainList.toArray(new X509Certificate[chainList.size()]);
        }
    }

    private static synchronized void addToTrustStore(String url, X509Certificate cert) throws GeneralSecurityException, IOException {
        FileObject root = FileUtil.getConfigRoot();
        FileObject ts = root.getFileObject(TRUST_STORE_PATH);

        char[] password = Keyring.read(TRUST_PASSWORD_KEY);
        if (password == null) {
            password = new BigInteger(130, RANDOM).toString(32).toCharArray();
            Keyring.save(TRUST_PASSWORD_KEY, password, null);
        }

        KeyStore keystore = KeyStore.getInstance("JKS"); // NOI18N
        InputStream is = (ts == null) ? null : new BufferedInputStream(ts.getInputStream());
        try {
            keystore.load(is, password);
        } catch (IOException ex) {
            LOGGER.log(Level.INFO, null, ex);
            // start from scratch
            keystore.load(null, null);
        } finally {
            if (is != null) {
                is.close();
            }
        }

        keystore.setCertificateEntry(url, cert); // NOI18N

        if (ts == null) {
            ts = FileUtil.createData(root, TRUST_STORE_PATH);
        }

        OutputStream out = new BufferedOutputStream(ts.getOutputStream());
        try {
            keystore.store(out, password);
        } finally {
            out.close();
        }
    }

    private static synchronized X509TrustManager createTrustManager(FileObject fo) throws GeneralSecurityException, IOException {
        KeyStore ts = KeyStore.getInstance("JKS"); // NOI18N
        InputStream in = new BufferedInputStream(fo.getInputStream());
        try {
            ts.load(in, null);
        } finally {
            in.close();
        }

        TrustManagerFactory tmf = TrustManagerFactory.getInstance(
                        TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ts);

        TrustManager[] tms = tmf.getTrustManagers();
        for (int i = 0; i < tms.length; i++) {
            if (tms[i] instanceof X509TrustManager) {
                return (X509TrustManager) tms[i];
            }
        }

        throw new NoSuchAlgorithmException("No X509TrustManager in TrustManagerFactory");
    }
}
