from __future__ import absolute_import

"""SSL Context

Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved."""

# M2Crypto
import sys

from M2Crypto import BIO, Err, RSA, X509, m2, util
from M2Crypto.SSL import cb
from weakref import WeakValueDictionary

__all__ = ['map', 'Context']

# Python 2 has int() and long().
# Python 3 and higher only has int().
# Work around this.
if sys.version_info > (3,):
    long = int


class _ctxmap:  # noqa
    singleton = None

    def __init__(self):
        self.map = WeakValueDictionary()

    def __getitem__(self, key):
        return self.map[key]

    def __setitem__(self, key, value):
        self.map[key] = value

    def __delitem__(self, key):
        del self.map[key]


def map():
    if _ctxmap.singleton is None:
        _ctxmap.singleton = _ctxmap()
    return _ctxmap.singleton


class Context:

    """'Context' for SSL connections."""

    m2_ssl_ctx_free = m2.ssl_ctx_free

    def __init__(self, protocol='sslv23', weak_crypto=None,
                 post_connection_check=None):
        proto = getattr(m2, protocol + '_method', None)
        if proto is None:
            raise ValueError("no such protocol '%s'" % protocol)
        self.ctx = m2.ssl_ctx_new(proto())
        self.allow_unknown_ca = 0
        self.post_connection_check = post_connection_check
        map()[long(self.ctx)] = self
        m2.ssl_ctx_set_cache_size(self.ctx, long(128))
        if weak_crypto is None:
            if protocol == 'sslv23':
                self.set_options(m2.SSL_OP_ALL | m2.SSL_OP_NO_SSLv2)
            self.set_cipher_list('ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH')

    def __del__(self):
        if getattr(self, 'ctx', None):
            self.m2_ssl_ctx_free(self.ctx)

    def close(self):
        del map()[long(self.ctx)]

    def load_cert(self, certfile, keyfile=None,
                  callback=util.passphrase_callback):
        """Load certificate and private key into the context.

        @param certfile: File that contains the PEM-encoded certificate.
        @type certfile:  str
        @param keyfile:  File that contains the PEM-encoded private key.
                         Default value of None indicates that the private key
                         is to be found in 'certfile'.
        @type keyfile:   str

        @param callback: Callable object to be invoked if the private key is
                         passphrase-protected. Default callback provides a
                         simple terminal-style input for the passphrase.
        """
        m2.ssl_ctx_passphrase_callback(self.ctx, callback)
        m2.ssl_ctx_use_cert(self.ctx, certfile)
        if not keyfile:
            keyfile = certfile
        m2.ssl_ctx_use_privkey(self.ctx, keyfile)
        if not m2.ssl_ctx_check_privkey(self.ctx):
            raise ValueError('public/private key mismatch')

    def load_cert_chain(self, certchainfile, keyfile=None,
                        callback=util.passphrase_callback):
        """Load certificate chain and private key into the context.

        @param certchainfile: File object containing the PEM-encoded
                              certificate chain.
        @type  certchainfile: str
        @param keyfile:       File object containing the PEM-encoded private
                              key. Default value of None indicates that the
                              private key is to be found in 'certchainfile'.
        @type keyfile:        str

        @param callback:      Callable object to be invoked if the private key
                              is passphrase-protected. Default callback
                              provides a simple terminal-style input for the
                              passphrase.
        """
        m2.ssl_ctx_passphrase_callback(self.ctx, callback)
        m2.ssl_ctx_use_cert_chain(self.ctx, certchainfile)
        if not keyfile:
            keyfile = certchainfile
        m2.ssl_ctx_use_privkey(self.ctx, keyfile)
        if not m2.ssl_ctx_check_privkey(self.ctx):
            raise ValueError('public/private key mismatch')

    def set_client_CA_list_from_file(self, cafile):
        """Load CA certs into the context. These CA certs are sent to the
        peer during *SSLv3 certificate request*.

        @param cafile: File object containing one or more PEM-encoded CA
                       certificates concatenated together.
        @type cafile:  str
        """
        m2.ssl_ctx_set_client_CA_list_from_file(self.ctx, cafile)

    # Deprecated.
    load_client_CA = load_client_ca = set_client_CA_list_from_file

    def load_verify_locations(self, cafile=None, capath=None):
        """Load CA certs into the context. These CA certs are used during
        verification of the peer's certificate.

        @param cafile: File containing one or more PEM-encoded CA certificates
                       concatenated together.
        @type cafile:  str
        @param capath: Directory containing PEM-encoded CA certificates
                       (one certificate per file).
        @type capath:  str
        """
        if cafile is None and capath is None:
            raise ValueError("cafile and capath can not both be None.")
        return m2.ssl_ctx_load_verify_locations(self.ctx, cafile, capath)

    # Deprecated.
    load_verify_info = load_verify_locations

    def set_session_id_ctx(self, id):
        ret = m2.ssl_ctx_set_session_id_context(self.ctx, id)
        if not ret:
            raise Err.SSLError(Err.get_error_code(), '')

    def set_allow_unknown_ca(self, ok):
        """Set the context to accept/reject a peer certificate if the
        certificate's CA is unknown.

        @param ok:       True to accept, False to reject.
        @type ok:        boolean
        """
        self.allow_unknown_ca = ok

    def get_allow_unknown_ca(self):
        """Get the context's setting that accepts/rejects a peer
        certificate if the certificate's CA is unknown.
        """
        return self.allow_unknown_ca

    def set_verify(self, mode, depth, callback=None):
        """
        Set verify options. Most applications will need to call this
        method with the right options to make a secure SSL connection.

        @param mode:     The verification mode to use. Typically at least
                         SSL.verify_peer is used. Clients would also typically
                         add SSL.verify_fail_if_no_peer_cert.
        @type mode:      int
        @param depth:    The maximum allowed depth of the certificate chain
                         returned by the peer.
        @type depth:     int
        @param callback: Callable that can be used to specify custom
                         verification checks.
        """
        if callback is None:
            m2.ssl_ctx_set_verify_default(self.ctx, mode)
        else:
            m2.ssl_ctx_set_verify(self.ctx, mode, callback)
        m2.ssl_ctx_set_verify_depth(self.ctx, depth)

    def get_verify_mode(self):
        return m2.ssl_ctx_get_verify_mode(self.ctx)

    def get_verify_depth(self):
        return m2.ssl_ctx_get_verify_depth(self.ctx)

    def set_tmp_dh(self, dhpfile):
        """Load ephemeral DH parameters into the context.

        @param dhpfile: File object containing the PEM-encoded DH
                        parameters.
        @type dhpfile:  str
        """
        f = BIO.openfile(dhpfile)
        dhp = m2.dh_read_parameters(f.bio_ptr())
        return m2.ssl_ctx_set_tmp_dh(self.ctx, dhp)

    def set_tmp_dh_callback(self, callback=None):
        if callback is not None:
            m2.ssl_ctx_set_tmp_dh_callback(self.ctx, callback)

    def set_tmp_rsa(self, rsa):
        """Load ephemeral RSA key into the context.

        @param rsa: M2Crypto.RSA.RSA instance.
        """
        if isinstance(rsa, RSA.RSA):
            return m2.ssl_ctx_set_tmp_rsa(self.ctx, rsa.rsa)
        else:
            raise TypeError("Expected an instance of RSA.RSA, got %s." % rsa)

    def set_tmp_rsa_callback(self, callback=None):
        if callback is not None:
            m2.ssl_ctx_set_tmp_rsa_callback(self.ctx, callback)

    def set_info_callback(self, callback=cb.ssl_info_callback):
        """
        Set a callback function that can be used to get state information
        about the SSL connections that are created from this context.

        @param callback: Callback function. The default prints information to
                         stderr.
        """
        m2.ssl_ctx_set_info_callback(self.ctx, callback)

    def set_cipher_list(self, cipher_list):
        return m2.ssl_ctx_set_cipher_list(self.ctx, cipher_list)

    def add_session(self, session):
        return m2.ssl_ctx_add_session(self.ctx, session._ptr())

    def remove_session(self, session):
        return m2.ssl_ctx_remove_session(self.ctx, session._ptr())

    def get_session_timeout(self):
        return m2.ssl_ctx_get_session_timeout(self.ctx)

    def set_session_timeout(self, timeout):
        return m2.ssl_ctx_set_session_timeout(self.ctx, timeout)

    def set_session_cache_mode(self, mode):
        return m2.ssl_ctx_set_session_cache_mode(self.ctx, mode)

    def get_session_cache_mode(self):
        return m2.ssl_ctx_get_session_cache_mode(self.ctx)

    def set_options(self, op):
        return m2.ssl_ctx_set_options(self.ctx, op)

    def get_cert_store(self):
        """
        Get the certificate store associated with this context.

        @warning: The store is NOT refcounted, and as such can not be relied
        to be valid once the context goes away or is changed.
        """
        return X509.X509_Store(m2.ssl_ctx_get_cert_store(self.ctx))
