package be.iminds.ilabt.jfed.lowlevel.connection;

import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.util.InetAddressUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.InvalidNameException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * Lots of code has been copied from DefaultHostnameVerifier
 */
class AliasAllowingHostnameVerifier implements HostnameVerifier {
    enum TYPE {IPv4, IPv6, DNS}


    final static int DNS_NAME_TYPE = 2;
    final static int IP_ADDRESS_TYPE = 7;

    private static final Logger LOG = LoggerFactory.getLogger(AliasAllowingHostnameVerifier.class);

    private final DefaultHostnameVerifier base = new DefaultHostnameVerifier();
    private final List<String> allowedCertificateHostnameAliases;

    AliasAllowingHostnameVerifier(List<String> allowedCertificateHostnameAliases) {
        this.allowedCertificateHostnameAliases = allowedCertificateHostnameAliases;
    }

    @Override
    public boolean verify(final String host, final SSLSession session) {
        try {
            final Certificate[] certs = session.getPeerCertificates();
            final X509Certificate x509 = (X509Certificate) certs[0];
            verify(host, x509);
            return true;
        } catch (final SSLException ex) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(ex.getMessage(), ex);
            }
            return false;
        }
    }

    public void verify(String host, X509Certificate cert) throws SSLException {
        if (allowedCertificateHostnameAliases.contains("*")) {
            LOG.debug("Allowing all certificates for host {}, as allowedCertificateHostnameAliases contains '*'", host);
            return;
        }


        TYPE hostFormat = TYPE.DNS;
        if (InetAddressUtils.isIPv4Address(host)) {
            hostFormat = TYPE.IPv4;
        } else {
            String s = host;
            if (s.startsWith("[") && s.endsWith("]")) {
                s = host.substring(1, host.length() - 1);
            }
            if (InetAddressUtils.isIPv6Address(s)) {
                hostFormat = TYPE.IPv6;
            }
        }

        final int subjectType =
                hostFormat == TYPE.IPv4 || hostFormat == TYPE.IPv6 ? IP_ADDRESS_TYPE : DNS_NAME_TYPE;
        final List<String> subjectAlts = extractSubjectAlts(cert, subjectType);
        String cn = "<NOT EXAMINED>";

        try {
            if (subjectAlts != null && !subjectAlts.isEmpty()) {

                if (subjectAlts.stream().anyMatch(allowedCertificateHostnameAliases::contains)) {
                    LOG.debug("Allowing certificate for host {}: a subjectAlt was found in allowedCertificateHostnameAliases", host);
                    return;
                }
            } else {
                // CN matching has been deprecated by rfc2818 and can be used
                // as fallback only when no subjectAlts are available
                final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
                cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
                if (cn == null) {
                    throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
                            "a common name and does not have alternative names");
                }
                if (allowedCertificateHostnameAliases.contains(cn)) {
                    LOG.debug("Allowing certificate for host {}: cn '{}' was found in allowedCertificateHostnameAliases", host, cn);
                    return;
                }
            }

            LOG.debug("no known alias in "+allowedCertificateHostnameAliases+" for "+subjectAlts+" -> using default HostnameVerifier");

            //fallback to base HostnameVerifier
            base.verify(host, cert);
        } catch (SSLException ex) {
            throw new HostnameAndCertificateMismatchSSLException(host, cn, subjectAlts, allowedCertificateHostnameAliases, ex);
        }


    }

    static String extractCN(final String subjectPrincipal) throws SSLException {
        if (subjectPrincipal == null) {
            return null;
        }
        try {
            final LdapName subjectDN = new LdapName(subjectPrincipal);
            final List<Rdn> rdns = subjectDN.getRdns();
            for (int i = rdns.size() - 1; i >= 0; i--) {
                final Rdn rds = rdns.get(i);
                final Attributes attributes = rds.toAttributes();
                final Attribute cn = attributes.get("cn");
                if (cn != null) {
                    try {
                        final Object value = cn.get();
                        if (value != null) {
                            return value.toString();
                        }
                    } catch (final NoSuchElementException | NamingException ignore) {
                    }
                }
            }
            return null;
        } catch (final InvalidNameException e) {
            throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
        }
    }

    static List<String> extractSubjectAlts(final X509Certificate cert, final int subjectType) {
        Collection<List<?>> c = null;
        try {
            c = cert.getSubjectAlternativeNames();
        } catch (final CertificateParsingException ignore) {
        }
        List<String> subjectAltList = null;
        if (c != null) {
            for (final List<?> aC : c) {
                final List<?> list = aC;
                final int type = (Integer) list.get(0);
                if (type == subjectType) {
                    final String s = (String) list.get(1);
                    if (subjectAltList == null) {
                        subjectAltList = new ArrayList<>();
                    }
                    subjectAltList.add(s);
                }
            }
        }
        return subjectAltList;
    }

}
