package be.iminds.ilabt.jfed.util;

import be.iminds.ilabt.jfed.lowlevel.connection.*;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.conn.HttpRoutedConnection;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * SSLCertificateDownloader downloads an SSL certificate, given an URL.
 *
 * based on InstallCert.java
 */
public class SSLCertificateDownloader {
    private static final Logger LOG = LoggerFactory.getLogger(SSLCertificateDownloader.class);
    private static final Pattern CN_PATTERN = Pattern.compile(".*CN=([^ ,]*)[ ,]*.*");

    /**
     * Certificate + additional derived info that might be useful
     * */
    public static class SSLCertificateJFedInfo {
        private X509Certificate cert;
        private X509Certificate[] certchain;
        private String urn;
        private String urnAuthPart;
        private String subject; //subject CN
        private Boolean selfSigned;
        private Boolean subjectMatchesHostname;
        private final URL url;
        private final String hostname;

        private final boolean connectionError;
        private final Exception connectionException;

        private boolean trusted;

        private SSLCertificateJFedInfo(X509Certificate cert, String urn, String urnAuthPart, String subject, boolean selfSigned, URL url, String hostname) {
            this.cert = cert;
            this.urn = urn;
            this.urnAuthPart = urnAuthPart;
            this.subject = subject;
            this.selfSigned = selfSigned;
            this.url = url;
            this.hostname = hostname;

            this.connectionError = false;
            this.connectionException = null;

            this.trusted = false;
        }
        public SSLCertificateJFedInfo(X509Certificate cert, URL url, String hostname) {
            this.cert = cert;
            this.urn = null;
            this.urnAuthPart = null;
            this.subject = null;
            this.selfSigned = null;
            this.subjectMatchesHostname = null;
            this.url = url;
            this.hostname = hostname;

            this.connectionError = false;
            this.connectionException = null;

            this.trusted = false;
        }
        private SSLCertificateJFedInfo(URL url, String hostname) {
            this.cert = null;
            this.urn = null;
            this.urnAuthPart = null;
            this.subject = null;
            this.selfSigned = null;
            this.subjectMatchesHostname = null;
            this.url = url;
            this.hostname = hostname;

            this.connectionError = false;
            this.connectionException = null;

            this.trusted = false;
        }
        private SSLCertificateJFedInfo(URL url, String hostname, Exception connectionException) {
            this.cert = null;
            this.urn = null;
            this.urnAuthPart = null;
            this.subject = null;
            this.selfSigned = null;
            this.subjectMatchesHostname = null;
            this.url = url;
            this.hostname = hostname;

            this.connectionError = true;
            this.connectionException = connectionException;

            this.trusted = false;
        }

        public X509Certificate getCert() {
            return cert;
        }

        public String getUrn() {
            return urn;
        }

        public String getUrnAuthPart() {
            return urnAuthPart;
        }

        public String getSubject() {
            return subject;
        }

        public Boolean isSelfSigned() {
            return selfSigned;
        }

        public Boolean getSubjectMatchesHostname() {
            return subjectMatchesHostname;
        }

        public X509Certificate[] getChain() {
            return certchain;
        }

        public X509Certificate getChainRoot() {
            if (certchain == null)
                return null;
            return certchain[certchain.length-1];
        }

        public URL getUrl() {
            return url;
        }

        public String getHostname() {
            return hostname;
        }

        public boolean isConnectionError() {
            return connectionError;
        }

        public Exception getConnectionException() {
            return connectionException;
        }

        @Override
        public String toString() {
            return "SSLCertificateJFedInfo{" +
                    "cert=" + (cert == null?"none":"present") +
                    ", certchain has " + (certchain == null ? null : certchain.length) + " elements" +
                    ", urn='" + urn + '\'' +
                    ", urnAuthPart='" + urnAuthPart + '\'' +
                    ", subject='" + subject + '\'' +
                    ", selfSigned=" + selfSigned +
                    ", subjectMatchesHostname=" + subjectMatchesHostname +
                    ", url=" + url +
                    ", hostname=" + hostname +
                    ", trusted=" + trusted +
                    '}';
        }

        public boolean isTrusted() {
            return trusted;
        }
    }


    public static class SavingTrustManager implements X509TrustManager {
        private final X509TrustManager tm;
        public X509Certificate[] chain;

        public SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }

        public X509Certificate[] getAcceptedIssuers() {
            throw new UnsupportedOperationException();
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            throw new UnsupportedOperationException();
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }

    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();

    private static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (int b : bytes) {
            b &= 0xff;
            sb.append(HEXDIGITS[b >> 4]);
            sb.append(HEXDIGITS[b & 15]);
            sb.append(' ');
        }
        return sb.toString();
    }



    static public X509Certificate getCertificate(URL url) {
        return getCertificate(url, null);
    }
    static public X509Certificate getCertificate(URL url, JFedConnection.SshProxyInfo proxyInfo) {
        SSLCertificateJFedInfo info = getCertificateInfo(url, proxyInfo);
        return info.getCert();
    }
    static public SSLCertificateJFedInfo getCertificateInfo(URL url) {
        return getCertificateInfo(url, null);
    }

    static private class CertificateInterceptor implements HttpResponseInterceptor {
        private List<X509Certificate> certificateList;
        private X509Certificate[] certificateArr;

        @Override
        public void process(HttpResponse httpResponse, HttpContext httpContext) throws HttpException, IOException {
            HttpRoutedConnection routedConnection = (HttpRoutedConnection) httpContext.getAttribute(ExecutionContext.HTTP_CONNECTION);
            Certificate[] certificates;
            if (routedConnection.getSSLSession() != null)
                certificates = routedConnection.getSSLSession().getPeerCertificates();
            else
                certificates = null;
            LOG.debug("processing HttpResponse. secure="+routedConnection.isSecure()+" certs:"+
                    (certificates == null ? null : Arrays.asList(certificates)));
            if (routedConnection.isSecure()) {
                if (certificates != null) {
                    certificateList = new ArrayList<>();
                    for (Certificate c : certificates)
                        if (c instanceof X509Certificate)
                            certificateList.add((X509Certificate)c);
                        else
                            LOG.warn("Not an X509 cert in chain: "+c);
                    certificateArr = new X509Certificate[certificateList.size()];
                    certificateArr = certificateList.toArray(certificateArr);
                }
//                else
//                    certificateList = null;
            }
        }
    }


    static public SSLCertificateJFedInfo getCertificateInfo(URL url, JFedConnection.SshProxyInfo proxyInfo) {
        try {
            boolean trusted = true;
            SSLCertificateJFedInfo res = new SSLCertificateJFedInfo(url, url.getHost());

            KeyStore systemTrustStore = JFedTrustStore.getSystemTrustStore();

            List<X509Certificate> chain;
            X509Certificate[] chainArr;
            //this uses a new method! this method is not yet used for all, needs to be tested first.
            if (proxyInfo != null)
                LOG.debug("getCertificateInfo with proxyInfo != null");
            else
                LOG.debug("getCertificateInfo with proxyInfo == null");
            ConnectionBuilder conBuilder = new BasicConnectionBuilderFactory().createConnectionBuilder();
            conBuilder.setProxy(proxyInfo, false);
            conBuilder.setUrl(url);
            //NOTE: HandleUntrustedCallback is NEEDED! Otherwise, the SpyingTrustStrategy is not used. See BasicConnectionBuilder for details.
            conBuilder.useHttps(new JFedTrustStore(systemTrustStore), sSLCertificateJFedInfo -> {
                return false; //no need to trust in this check
            });
            conBuilder.useNoAuthentication();
            conBuilder.setHackAllowAllServerCertificates(true);
            conBuilder.setDebugInfo(new JFedConnection.DebugInfo(null, null));
            HttpConnection con = conBuilder.buildHttpConnection();
            CertificateInterceptor ci = new CertificateInterceptor();
            ((DefaultHttpClient)con.getHttpClient()).addResponseInterceptor(ci);
            HttpOptions options = new HttpOptions(url.toURI());

            LOG.debug("Setting up connection just to fetch SSL Certificate info. url="+url);
            try {
                con.getHttpClient().execute(options, con.getHttpContext());
            } catch (SSLPeerUnverifiedException e) {
                LOG.debug("SSLPeerUnverifiedException executing OPTIONS. This can be ignored.", e);
                trusted = false;
            } catch (IOException e) {
                LOG.debug("Exception executing OPTIONS. This can be ignored.", e);
            }


            chain = ci.certificateList;
            chainArr = ci.certificateArr;
            LOG.debug("Got peer certificates from interceptor:" +
                    (chain != null ? Collections.singletonList(chain) : null));

            if (chainArr == null && con.getServerCertificates() != null) {
                chainArr = con.getServerCertificates();
                chain = Arrays.asList(chainArr);
            }

            LOG.debug("Got peer certificates from spy:" +
                    (chain != null ? Collections.singletonList(chain) : null));

            if (chainArr == null || chain == null) {
                LOG.error("Could not obtain server certificate chain");
                return res; // res is all null
            }


            Boolean selfSigned = false;
            for (X509Certificate cert : chain) {

                try {
                    Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
                    if (altNames != null)
                        for (List<?> altName : altNames) {
                            Integer nameType = (Integer) altName.get(0);
                            if (nameType == 6) {
                                //uniformResourceIdentifier
                                String urn = (String) altName.get(1);
                                GeniUrn geniUrn = GeniUrn.parse(urn);
                                if (geniUrn == null)
                                    LOG.warn("Warning: certificate alternative name URI is not a valid authority urn: \"" + urn + "\"  (will be ignored)");
                                else {
                                    res.urnAuthPart = geniUrn.getEncodedTopLevelAuthority();
                                    res.urn = urn;
                                }
                            }
                        }
                } catch (CertificateParsingException ignored) {
                }

                if (Objects.equals(cert.getIssuerDN(), cert.getSubjectDN())) {
                    LOG.debug("Certificate has same issues and subject -> it's self signed!");
                    selfSigned = true;
                }

                boolean selfVerifyFailed = false;
                try {
                    cert.verify(cert.getPublicKey());
                } catch (CertificateException | SignatureException | NoSuchProviderException | InvalidKeyException | NoSuchAlgorithmException e) {
                    selfVerifyFailed = true;
                }
                if (!selfVerifyFailed) {
                    LOG.debug("Certificate has signed itself -> it's self signed!");
                    selfSigned = true;
                }
            }

            res.cert = chainArr[0];
            res.certchain = chainArr;

            Matcher matchCN = CN_PATTERN.matcher(res.cert.getSubjectX500Principal().toString());
            res.subject = matchCN.matches() ? matchCN.group(1) : null;
            res.subjectMatchesHostname = Objects.equals(res.subject, url.getHost());

            if (!res.subjectMatchesHostname)
                trusted = false;

            res.trusted = trusted;
            res.selfSigned = selfSigned;

            return res;
        } catch (Exception e) {
            LOG.info("Failed to fetch SSL certificate: " + e.getMessage(), e);
            return new SSLCertificateJFedInfo(url, url.getHost(), e); // res is all null
        }
    }

    public static void main(String [] args) throws MalformedURLException {
        SSLCertificateJFedInfo cert1 = SSLCertificateDownloader.getCertificateInfo(new URL("https://www.wall3.test.ibbt.be/"));
        System.out.println("returned cert: "+cert1);
//        SSLCertificateJFedInfo cert2 = SSLCertificateDownloader.getCertificateInfo(new URL("https://sfa.planet-lab.eu:12346"));
//        System.out.println("returned cert: "+cert2);
//        SSLCertificateJFedInfo cert3 = SSLCertificateDownloader.getCertificateInfo(new URL("http://sfav3.planet-lab.org:12346"));
//        System.out.println("returned cert: "+cert3);
//        SSLCertificateJFedInfo cert4 = SSLCertificateDownloader.getCertificateInfo(new URL("https://fiteagle.av.tu-berlin.de:12346"));
//        System.out.println("returned cert: "+cert4);
//        SSLCertificateJFedInfo cert5 = SSLCertificateDownloader.getCertificateInfo(new URL("https://fiteagle.av.tu-berlin.de:12347"));
//        System.out.println("returned cert: "+cert5);
    }
}
