#!python

import argparse
import datetime
import socket
import ssl
import json

VERSION = "v1.4.0"
RELEASE = "2020-05-31"

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
DEFAULT_TIMEOUT = 3
DEFAULT_OUTPUT = "text"


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)
    parser.add_argument('--timeout', help="Timeout for check in seconds, default {}".format(DEFAULT_TIMEOUT),
                        type=int, default=DEFAULT_TIMEOUT)
    parser.add_argument('--output', help="Output format (text, json, nagios), default {}".format(DEFAULT_OUTPUT),
                        type=str, default=DEFAULT_OUTPUT)
    args = parser.parse_args()

    return args


def monitor(hostname: str, port: int, timeout: int) -> dict:
    """
    Returns the number of days until ssl certificate expires

    :param hostname:
    :param port:
    :param timeout:
    :return:
    """

    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=hostname)

    conn.settimeout(timeout)
    conn.connect((hostname, port))
    ssl_data = conn.getpeercert()

    issuer = "issuer not known"
    now = datetime.datetime.utcnow()
    expire = datetime.datetime.strptime(ssl_data['notAfter'], ssl_date_fmt)

    for ix in ssl_data['issuer']:
        if ix[0][0] == "commonName":
            issuer = ix[0][1]

    return {
        "expiration": expire - now,
        "issuer_common_name": issuer
    }


def output_text(ssl_check):
    """
    Output text
    :param ssl_check: ssl_exp
    :return:
    """

    return ssl_check['message']


def output_json(ssl_check):
    """
    Output json
    :param ssl_check: ssl_exp
    :return:
    """

    ssl_check.update(expiration=str(ssl_check['expiration']))

    return json.dumps(ssl_check)


def output_nagios(ssl_check, warn: int, crit: int):
    """
    Output nagios
    :param ssl_check: ssl_exp
    :param warn:
    :param crit:
    :return:
    """

    if ssl_check['code'] == 0:
        state = 'OK'
    elif ssl_check['code'] == 1:
        state = 'WARNING'
    elif ssl_check['code'] == 2:
        state = 'CRITICAL'
    elif ssl_check['code'] == 3:
        state = 'UNKNOWN'
    else:
        state = 'UNKNOWN'

    days = ssl_check['expiration'].days
    label = "SSL certificate {state} - days: {days} | days={days};{warning};{critical};{min};{max}"

    return label.format(
        state=state, days=days, warning=warn, critical=crit, min=days, max=days
    )


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

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

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

    try:
        check = monitor(hostname=domain, port=port, timeout=timeout)
        exp_time = check.get("expiration")
        issuer = check.get("issuer_common_name")
        result.update(expiration=exp_time)
    except ssl.CertificateError as e:
        result.update(message="UNKNOWN: ssl certificate error, {exception}".format(exception=e))
    except ssl.SSLError as e:
        result.update(message="UNKNOWN: ssl error, {exception}".format(exception=e))
    except socket.timeout as e:
        result.update(message="UNKNOWN: socket timeout, {exception}".format(exception=e))
    except socket.error as e:
        result.update(message="UNKNOWN: socket error, {exception}".format(exception=e))
    else:
        if exp_time < datetime.timedelta(days=0):
            result.update(message="CRITICAL: SSL {domain} has expired!".format(
                domain=domain), code=STATE_CRITICAL)
        elif exp_time < datetime.timedelta(days=critical):
            result.update(message="CRITICAL: SSL {domain} ({issuer}) will expire in {expiration}".format(
                domain=domain, expiration=exp_time, issuer=issuer), code=STATE_CRITICAL)
        elif exp_time < datetime.timedelta(days=warning):
            result.update(message="WARNING: SSL {domain} ({issuer}) will expire in {expiration}".format(
                domain=domain, expiration=exp_time, issuer=issuer), code=STATE_WARNING)
        else:
            result.update(message="OK: SSL {domain} ({issuer}) will expire in {expiration}".format(
                domain=domain, expiration=exp_time, issuer=issuer), code=STATE_OK)

    return result


if __name__ == "__main__":
    # parse cmd args
    parse_args = arg_parse()
    wrn = int(parse_args.warning)
    crt = int(parse_args.critical)

    # check ssl cert
    cert_check = ssl_exp(
        domain=parse_args.url,
        critical=crt,
        warning=wrn,
        port=int(parse_args.port),
        timeout=int(parse_args.timeout)
    )

    exit_code = cert_check['code']

    # output in defined format
    if parse_args.output == "text":
        output = output_text(cert_check)
    elif parse_args.output == "json":
        output = output_json(cert_check)
    elif parse_args.output == "nagios":
        output = output_nagios(cert_check, wrn, crt)
    else:
        # default: text
        output = output_text(cert_check)

    print(output)
    exit(exit_code)
