#!python

import argparse
import datetime
import socket
import ssl

VERSION = "v1.1.0"
RELEASE = "2020-05-30"

DESCRIPTION = """
This program checks the expiration date of an ssl certificate.
First set the url param that should contain the url address of a domain.
The program returns a message and a status code based on a measurement result.
Release: {release}
""".format(release=RELEASE)

STATE_OK = 0
STATE_WARNING = 1
STATE_CRITICAL = 2
STATE_UNKNOWN = 3

DEFAULT_WARNING = 30
DEFAULT_CRITICAL = 20
DEFAULT_SSL_PORT = 443


def arg_parse() -> argparse:
    """
    Parse input arguments

    :return:
    """

    parser = argparse.ArgumentParser(description=DESCRIPTION)
    parser.add_argument('--version', action='version', version='%(prog)s {version}'.format(version=VERSION))
    parser.add_argument('--url', help="URL of ssl certificate for check", required=True)
    parser.add_argument('--warning', help="Number of days for warning output, default {}".format(DEFAULT_WARNING),
                        type=int, default=DEFAULT_WARNING)
    parser.add_argument('--critical', help="Number of days for critical output, default {}".format(DEFAULT_CRITICAL),
                        type=int, default=DEFAULT_CRITICAL)
    parser.add_argument('--port', help="SSL port, default {}".format(DEFAULT_SSL_PORT),
                        type=int, default=DEFAULT_SSL_PORT)
    args = parser.parse_args()

    return args


def ssl_expiration_datetime(domain: str, timeout: int = 3.0, ssl_port: int = DEFAULT_SSL_PORT) -> datetime.datetime:
    """
    Get ssl certificate expiration date

    :param domain:
    :param timeout:
    :param ssl_port:
    :return: datetime.datetime
    """

    ssl_date_fmt = r'%b %d %H:%M:%S %Y %Z'

    context = ssl.create_default_context()
    conn = context.wrap_socket(socket.socket(socket.AF_INET), server_hostname=domain)
    conn.settimeout(timeout)

    conn.connect((domain, ssl_port))
    ssl_info = conn.getpeercert()

    return datetime.datetime.strptime(ssl_info['notAfter'], ssl_date_fmt)


def ssl_life_time_remaining(hostname: str, port: int) -> datetime.timedelta:
    """
    Returns the number of days until ssl certificate expires

    :param hostname:
    :param port:
    :return: datetime.timedelta
    """

    now = datetime.datetime.utcnow()
    expires = ssl_expiration_datetime(domain=hostname, ssl_port=port)

    return expires - now


def check_domain_ssl(domain: str, critical: int = DEFAULT_CRITICAL, warning: int = DEFAULT_WARNING,
                     port: int = DEFAULT_SSL_PORT) -> dict:
    """
    Returns output message a status code based on domain measurement

    :param domain:
    :param critical:
    :param warning:
    :param port:
    :return:
    """

    result = {"message": "empty", "code": STATE_UNKNOWN}

    try:
        exp_time = ssl_life_time_remaining(domain, port)
    except ssl.CertificateError as e:
        result.update(message="UNKNOWN: ssl certificate error, {exception}".format(exception=e))
        return result
    except ssl.SSLError as e:
        result.update(message="UNKNOWN: ssl error, {exception}".format(exception=e))
        return result
    except socket.timeout as e:
        result.update(message="UNKNOWN: socket timeout, {exception}".format(exception=e))
        return result
    except socket.error as e:
        result.update(message="UNKNOWN: socket error, {exception}".format(exception=e))
        return result
    else:
        if exp_time < datetime.timedelta(days=0):
            result.update(message="CRITICAL: ssl certificate for {domain} has expired".format(
                domain=domain), code=STATE_CRITICAL)
            return result
        elif exp_time < datetime.timedelta(days=critical):
            result.update(message="CRITICAL: ssl certificate for {domain} will expire in {expiration}".format(
                domain=domain, expiration=exp_time), code=STATE_CRITICAL)
            return result
        elif exp_time < datetime.timedelta(days=warning):
            result.update(message="WARNING: ssl certificate for {domain} will expire in {expiration}".format(
                domain=domain, expiration=exp_time), code=STATE_WARNING)
            return result
        else:
            result.update(message="OK: ssl certificate for {domain} is ok and will expire in {expiration}".format(
                domain=domain, expiration=exp_time), code=STATE_OK)
            return result


if __name__ == "__main__":
    parse_args = arg_parse()
    domain_certificate_state = check_domain_ssl(
        parse_args.url, int(parse_args.critical), int(parse_args.warning), int(parse_args.port)
    )

    print(domain_certificate_state['message'])
    exit(domain_certificate_state['code'])
