
from __future__ import absolute_import
import logging
import socket
import ssl
from netaddr import IPAddress
from netaddr.core import AddrFormatError

from ssl import SSLError
from vdsm.common import pki
from vdsm.common.time import monotonic_time
from .config import config

CLIENT_PROTOCOL = ssl.PROTOCOL_SSLv23


class SSLSocket(object):
    def __init__(self, sock):
        self.sock = sock
        self._data = ''

    def read(self, size=4096, flag=None):
        result = None
        try:
            if flag == socket.MSG_PEEK:
                bytes_left = size - len(self._data)
                if bytes_left > 0:
                    self._data += self.sock.read(bytes_left)
                result = self._data
            else:
                if self._data:
                    result = self._data
                    self._data = ''
                else:
                    result = self.sock.read(size)
        except SSLError as e:
            if e.errno != ssl.SSL_ERROR_WANT_READ:
                raise

        return result
    recv = read

    def pending(self):
        pending = self.sock.pending()
        if self._data:
            pending = pending + len(self._data)
        return pending

    def __getattr__(self, name):
        return getattr(self.sock, name)

    def makefile(self, mode='rb', bufsize=-1):
        if mode == 'rb':
            return socket._fileobject(self, mode, bufsize)
        else:
            return self.sock.makefile(mode, bufsize)


class SSLContext(object):

    def __init__(self, cert_file, key_file, ca_certs=None,
                 excludes=0, protocol=CLIENT_PROTOCOL):
        self.cert_file = cert_file
        self.key_file = key_file
        self.ca_certs = ca_certs
        self.excludes = excludes
        self.protocol = protocol

    def wrapSocket(self, sock):
        ciphers = config.get('vars', 'ssl_ciphers')
        return SSLSocket(
            ssl.wrap_socket(sock,
                            keyfile=self.key_file,
                            certfile=self.cert_file,
                            server_side=False,
                            cert_reqs=ssl.CERT_REQUIRED,
                            ssl_version=self.protocol,
                            ca_certs=self.ca_certs,
                            ciphers=ciphers if ciphers else None)
        )


class SSLHandshakeDispatcher(object):
    log = logging.getLogger("ProtocolDetector.SSLHandshakeDispatcher")
    SSL_HANDSHAKE_TIMEOUT = 10
    LOCAL_ADDRESSES = ('127.0.0.1', '::1')

    def __init__(
        self,
        sslctx,
        handshake_finished_handler,
        handshake_timeout=SSL_HANDSHAKE_TIMEOUT,
    ):
        self._give_up_at = monotonic_time() + handshake_timeout
        self._has_been_set_up = False
        self._is_handshaking = True
        self.want_read = True
        self.want_write = True
        self._sslctx = sslctx
        self._handshake_finished_handler = handshake_finished_handler

    def _set_up_socket(self, dispatcher):
        client_socket = dispatcher.socket

        context = ssl.SSLContext(self._sslctx.protocol)
        ciphers = config.get('vars', 'ssl_ciphers')
        if ciphers:
            context.set_ciphers(ciphers)
        context.options |= ssl.OP_NO_SSLv3

        excludes = self._sslctx.excludes
        if excludes != 0:
            context.options |= excludes

        context.load_verify_locations(self._sslctx.ca_certs, None, None)
        context.verify_mode = ssl.CERT_REQUIRED
        context.load_cert_chain(self._sslctx.cert_file, self._sslctx.key_file)

        client_socket = SSLSocket(
            context.wrap_socket(client_socket,
                                server_side=True,
                                do_handshake_on_connect=False))

        dispatcher.socket = client_socket
        self._has_been_set_up = True

    def next_check_interval(self):
        return max(self._give_up_at - monotonic_time(), 0)

    def readable(self, dispatcher):
        if self.has_expired():
            dispatcher.close()
            return False

        return self.want_read

    def writable(self, dispatcher):
        if self.has_expired():
            dispatcher.close()
            return False

        return self.want_write

    def has_expired(self):
        return monotonic_time() >= self._give_up_at

    def handle_read(self, dispatcher):
        self._handle_io(dispatcher)

    def handle_write(self, dispatcher):
        self._handle_io(dispatcher)

    def _handle_io(self, dispatcher):
        if not self._has_been_set_up:
            self._set_up_socket(dispatcher)

        if self._is_handshaking:
            self._handshake(dispatcher)
        else:
            if config.getboolean('vars', 'verify_client_cert'):
                peercert = dispatcher.socket.getpeercert()
                peername = dispatcher.socket.getpeername()[0]
                if not self._verify_host(peercert, peername):
                    self.log.error(
                        "peer certificate '%s' does not match host name '%s'",
                        peercert, peername)
                    dispatcher.socket.close()
                    return

            self._handshake_finished_handler(dispatcher)

    def _verify_host(self, peercert, addr):
        if not peercert:
            return False

        for sub in peercert.get("subject", ()):
            for key, value in sub:
                if key == "commonName":
                    return self.compare_names(addr, value)

        return False

    @staticmethod
    def compare_names(src_addr, cert_common_name):
        src_addr = SSLHandshakeDispatcher._normalize_ip_address(src_addr)
        try:
            cert_common_name = \
                SSLHandshakeDispatcher._normalize_ip_address(
                    cert_common_name)
        except AddrFormatError:
            pass

        if src_addr == cert_common_name:
            return True
        elif src_addr in SSLHandshakeDispatcher.LOCAL_ADDRESSES:
            return True
        else:
            name, aliaslist, addresslist = socket.gethostbyaddr(src_addr)
            hostnames = [name] + aliaslist + addresslist

            return any(cert_common_name.lower() == hostname.lower()
                       for hostname in hostnames)

    @staticmethod
    def _normalize_ip_address(addr):
        ip = IPAddress(addr)
        if ip.is_ipv4_mapped():
            addr = str(ip.ipv4())

        return addr

    def _handshake(self, dispatcher):
        try:
            dispatcher.socket.do_handshake()
        except SSLError as err:
            self.want_read = self.want_write = False
            if err.args[0] == ssl.SSL_ERROR_WANT_READ:
                self.want_read = True
            elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
                self.want_write = True
            else:
                self.log.error("ssl handshake: SSLError, address: {}".format(
                    dispatcher.socket.getpeername()[0]))
                dispatcher.close()
        except socket.error:
            self.log.error("ssl handshake: socket error, address: {}".format(
                dispatcher.socket.getpeername()[0]))
            dispatcher.close()
        else:
            self.want_read = self.want_write = True
            self._is_handshaking = False

    def handle_close(self, dispatcher):
        dispatcher.close()


def create_ssl_context():
        sslctx = None
        if config.getboolean('vars', 'ssl'):
            protocol = (
                ssl.PROTOCOL_SSLv23
                if config.get('vars', 'ssl_protocol') == 'sslv23'
                else ssl.PROTOCOL_TLSv1_2
            )

            excludes = protocol_name_to_int()
            sslctx = SSLContext(key_file=pki.KEY_FILE,
                                cert_file=pki.CERT_FILE,
                                ca_certs=pki.CA_FILE,
                                protocol=protocol,
                                excludes=excludes)
        return sslctx


def protocol_name_to_int():
    excludes = 0

    for no_protocol in config.get('vars', 'ssl_excludes').split(','):
        if no_protocol != '':
            excludes |= getattr(ssl, no_protocol.strip())

    excludes |= getattr(ssl, 'OP_NO_TLSv1')
    excludes |= getattr(ssl, 'OP_NO_TLSv1_1')

    return excludes
