#!/usr/bin/env python3

import ipaddress
import socket
import ssl

import click
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509 import NameOID


def cert_get_names(cert_data):

    try:
        cert = x509.load_pem_x509_certificate(cert_data, default_backend())
    except ValueError:
        pass

    try:
        cert = x509.load_der_x509_certificate(cert_data, default_backend())
    except ValueError:
        raise ValueError("No recognized cert format. Allowed: PEM or DER")

    names = set()
    names.add(cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value.lower())

    try:
        alt_names = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
    except x509.extensions.ExtensionNotFound:
        alt_names = None

    if alt_names:
        for alt_name in alt_names.value.get_values_for_type(x509.DNSName):
            names.add(alt_name.lower())

    return list(sorted(names))


@click.command()
@click.option('-p', 'ports', default='443', help='Ports to connect to (comma separated list)')
@click.option('-i', 'infile', type=click.File('r'), default='-', help='Input file (Default: stdin)')
@click.option('-t', 'timeout', type=click.FLOAT, default=1, help='Time to wait for each connection')
@click.option('-v', 'verbose', is_flag=True, default=False, help='Verbose output')
@click.argument('network', default=None, required=False)
def cmd_cert_names(infile, ports, timeout, verbose, network):
    """
    Connects to each host/port and shows a summary of the certificate names.

    The hosts to connect to are taken from two possible options:

    1) -i option (stdin by default). A file where each line is a host or network
    2) An argument that can be a host or network

    If you use both methods, the hosts and networks are merged into one list.

    Example:

    \b
    $ habu.cert.names 2.18.60.240/29
    2.18.60.241         443 i.s-microsoft.com microsoft.com privacy.microsoft.com
    2.18.60.242         443 aod-ssl.itunes.apple.com aod.itunes.apple.com aodp-ssl.itunes.apple.com
    2.18.60.243         443 *.mlb.com mlb.com
    2.18.60.244         443 [SSL: TLSV1_ALERT_INTERNAL_ERROR] tlsv1 alert internal error (_ssl.c:1056)
    2.18.60.245         443 cert2-cn-public-ubiservices.ubi.com cert2-cn-public-ws-ubiservices.ubi.com
    2.18.60.246         443 *.blog.sina.com.cn *.dmp.sina.cn

    aod.itunes.apple.com
    aodp-ssl.itunes.apple.com
    aod-ssl.itunes.apple.com
    *.blog.sina.com.cn
    cert2-cn-public-ubiservices.ubi.com
    cert2-cn-public-ws-ubiservices.ubi.com
    *.dmp.sina.cn
    i.s-microsoft.com microsoft.com
    *.mlb.com mlb.com
    privacy.microsoft.com
    """

    ports = [ int(port) for port in ports.split(',') if int(port) > 0 and int(port) <= 65535 ]

    hosts = set()

    if network:
        try:
            network = ipaddress.ip_network(network, strict=False)
        except Exception as e:
            click.echo(e, err=True)
            return False

        if len(list(network.hosts())) == 0:
            hosts |= set([ipaddress.ip_address(str(network).split('/')[0])])
        else:
            hosts |= { host for host in network.hosts() }

    if not infile.isatty():
        for network in infile.read().split('\n'):
            if network:
                try:
                    network = ipaddress.ip_network(network, strict=False)
                    hosts |= { host for host in network.hosts() }
                except Exception:
                    click.echo('Ignoring invalid host/network: {}'.format(network), err=True)
                    continue

    hosts = sorted(hosts)
    all_names = set()

    for host in hosts:
        for port in ports:
            print('{:<16} {:>6}'.format(str(host), port), end=' ')

            context = ssl.create_default_context()
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE

            try:
                with socket.create_connection((str(host), int(port)), timeout=timeout) as sock:
                    with context.wrap_socket(sock) as ssock:
                        cert = ssock.getpeercert(binary_form=True)
                        names = cert_get_names(cert)
                        all_names |= set(names)
                        print(' '.join(names))
            except Exception as e:
                print(e)

    print('')
    print('\n'.join(sorted(all_names)))


if __name__ == '__main__':
    cmd_cert_names()
