# Copyright (c) 2012 - 2015 EMC Corporation.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import base64
import os
import socket
import ssl
import string
import struct

from eventlet import patcher
try:
    import OpenSSL
except ImportError:
    OpenSSL = None
from oslo_log import log as logging
import six
from six.moves import http_client
from six.moves import urllib

from cinder.i18n import _, _LI

# Handle case where we are running in a monkey patched environment
if OpenSSL and patcher.is_monkey_patched('socket'):
    from eventlet.green.OpenSSL import SSL

try:
    import pywbem
    pywbemAvailable = True
except ImportError:
    pywbemAvailable = False


LOG = logging.getLogger(__name__)


def to_bytes(s):
    if isinstance(s, six.string_types):
        return six.b(s)
    else:
        return s


def get_default_ca_certs():
    """Gets the default CA certificates if found, otherwise None.

    Try to find out system path with ca certificates. This path is cached and
    returned. If no path is found out, None is returned.
    """
    if not hasattr(get_default_ca_certs, '_path'):
        for path in (
                '/etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt',
                '/etc/ssl/certs',
                '/etc/ssl/certificates'):
            if os.path.exists(path):
                get_default_ca_certs._path = path
                break
        else:
            get_default_ca_certs._path = None
    return get_default_ca_certs._path


class OpenSSLConnectionDelegator(object):
    """An OpenSSL.SSL.Connection delegator.

    Supplies an additional 'makefile' method which http_client requires
    and is not present in OpenSSL.SSL.Connection.
    Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
    a delegator must be used.
    """
    def __init__(self, *args, **kwargs):
        self.connection = SSL.GreenConnection(*args, **kwargs)

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

    def makefile(self, *args, **kwargs):
        return socket._fileobject(self.connection, *args, **kwargs)


class HTTPSConnection(http_client.HTTPSConnection):
    def __init__(self, host, port=None, key_file=None, cert_file=None,
                 strict=None, ca_certs=None, no_verification=False):
        if not pywbemAvailable:
            LOG.info(_LI(
                'Module PyWBEM not installed.  '
                'Install PyWBEM using the python-pywbem package.'))
        if six.PY3:
            excp_lst = (TypeError, ssl.SSLError)
        else:
            excp_lst = ()
        try:
            http_client.HTTPSConnection.__init__(self, host, port,
                                                 key_file=key_file,
                                                 cert_file=cert_file)

            self.key_file = None if key_file is None else key_file
            self.cert_file = None if cert_file is None else cert_file
            self.insecure = no_verification
            self.ca_certs = (
                None if ca_certs is None else six.text_type(ca_certs))
            self.set_context()
            # ssl exceptions are reported in various form in Python 3
            # so to be compatible, we report the same kind as under
            # Python2
        except excp_lst as e:
            raise pywbem.cim_http.Error(six.text_type(e))

    @staticmethod
    def host_matches_cert(host, x509):
        """Verify that the certificate matches host.

        Verify that the x509 certificate we have received
        from 'host' correctly identifies the server we are
        connecting to, ie that the certificate's Common Name
        or a Subject Alternative Name matches 'host'.
        """
        def check_match(name):
            # Directly match the name.
            if name == host:
                return True

            # Support single wildcard matching.
            if name.startswith('*.') and host.find('.') > 0:
                if name[2:] == host.split('.', 1)[1]:
                    return True

        common_name = x509.get_subject().commonName
        # First see if we can match the CN.
        if check_match(common_name):
            return True
            # Also try Subject Alternative Names for a match.
        san_list = None
        for i in range(x509.get_extension_count()):
            ext = x509.get_extension(i)
            if ext.get_short_name() == b'subjectAltName':
                san_list = six.text_type(ext)
                for san in ''.join(san_list.split()).split(','):
                    if san.startswith('DNS:'):
                        if check_match(san.split(':', 1)[1]):
                            return True

        # Server certificate does not match host.
        msg = (_("Host %(host)s does not match x509 certificate contents: "
                 "CommonName %(commonName)s.")
               % {'host': host,
                  'commonName': common_name})

        if san_list is not None:
            msg = (_("%(message)s, subjectAltName: %(sanList)s.")
                   % {'message': msg,
                      'sanList': san_list})
        raise pywbem.cim_http.AuthError(msg)

    def verify_callback(self, connection, x509, errnum,
                        depth, preverify_ok):
        if x509.has_expired():
            msg = msg = (_("SSL Certificate expired on %s.")
                         % x509.get_notAfter())
            raise pywbem.cim_http.AuthError(msg)

        if depth == 0 and preverify_ok:
            # We verify that the host matches against the last
            # certificate in the chain.
            return self.host_matches_cert(self.host, x509)
        else:
            # Pass through OpenSSL's default result.
            return preverify_ok

    def set_context(self):
        """Set up the OpenSSL context."""
        self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)

        if self.insecure is not True:
            self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
                                    self.verify_callback)
        else:
            self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
                                    lambda *args: True)

        if self.cert_file:
            try:
                self.context.use_certificate_file(self.cert_file)
            except Exception as e:
                msg = (_("Unable to load cert from %(cert)s %(e)s.")
                       % {'cert': self.cert_file,
                          'e': e})
                raise pywbem.cim_http.AuthError(msg)
            if self.key_file is None:
                # We support having key and cert in same file.
                try:
                    self.context.use_privatekey_file(self.cert_file)
                except Exception as e:
                    msg = (_("No key file specified and unable to load key "
                             "from %(cert)s %(e)s.")
                           % {'cert': self.cert_file,
                              'e': e})
                    raise pywbem.cim_http.AuthError(msg)

        if self.key_file:
            try:
                self.context.use_privatekey_file(self.key_file)
            except Exception as e:
                msg = (_("Unable to load key from %(cert)s %(e)s.")
                       % {'cert': self.cert_file,
                          'e': e})
                raise pywbem.cim_http.AuthError(msg)

        if self.ca_certs:
            try:
                self.context.load_verify_locations(to_bytes(self.ca_certs))
            except Exception as e:
                msg = (_("Unable to load CA from %(cert)s %(e)s.")
                       % {'cert': self.cert_file,
                          'e': e})
                raise pywbem.cim_http.AuthError(msg)
        else:
            self.context.set_default_verify_paths()

    def connect(self):
        result = socket.getaddrinfo(self.host, self.port, 0,
                                    socket.SOCK_STREAM)
        if result:
            socket_family = result[0][0]
            if socket_family == socket.AF_INET6:
                sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
            else:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        else:
            # If due to some reason the address lookup fails - we still
            # connect to IPv4 socket. This retains the older behavior.
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        if self.timeout is not None:
            # '0' microseconds
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
                            struct.pack('LL', 0, 0))
        self.sock = OpenSSLConnectionDelegator(self.context, sock)
        self.sock.connect((self.host, self.port))


def wbem_request(url, data, creds, headers=None, debug=0, x509=None,
                 verify_callback=None, ca_certs=None,
                 no_verification=False):
    """Send request over HTTP.

    Send XML data over HTTP to the specified url. Return the
    response in XML.  Uses Python's build-in http_client.  x509 may be a
    dictionary containing the location of the SSL certificate and key
    files.
    """

    if headers is None:
        headers = []

    host, port, use_ssl = pywbem.cim_http.parse_url(url)
    key_file = None
    cert_file = None
    if use_ssl and x509 is not None:
        cert_file = x509.get('cert_file')
        key_file = x509.get('key_file')

    numTries = 0
    localAuthHeader = None
    tryLimit = 5

    if isinstance(data, six.text_type):
        data = data.encode('utf-8')
    data = '<?xml version="1.0" encoding="utf-8" ?>\n' + data

    if not no_verification and ca_certs is None:
        ca_certs = get_default_ca_certs()
    elif no_verification:
        ca_certs = None

    if use_ssl:
        h = HTTPSConnection(
            host,
            port=port,
            key_file=key_file,
            cert_file=cert_file,
            ca_certs=ca_certs,
            no_verification=no_verification)

    locallogin = None
    while numTries < tryLimit:
        numTries = numTries + 1

        h.putrequest('POST', '/cimom')
        h.putheader('Content-type', 'application/xml; charset="utf-8"')
        h.putheader('Content-length', len(data))
        if localAuthHeader is not None:
            h.putheader(*localAuthHeader)
        elif creds is not None:
            h.putheader('Authorization', 'Basic %s' %
                        base64.encodestring('%s:%s' % (creds[0], creds[1]))
                        .replace('\n', ''))
        elif locallogin is not None:
            h.putheader('PegasusAuthorization', 'Local "%s"' % locallogin)

        for hdr in headers:
            if isinstance(hdr, six.text_type):
                hdr = hdr.encode('utf-8')
            s = map(lambda x: string.strip(x), string.split(hdr, ":", 1))
            h.putheader(urllib.parse.quote(s[0]), urllib.parse.quote(s[1]))

        try:
            h.endheaders()
            try:
                h.send(data)
            except socket.error as arg:
                if arg[0] != 104 and arg[0] != 32:
                    raise

            response = h.getresponse()
            body = response.read()

            if response.status != 200:
                raise pywbem.cim_http.Error('HTTP error')

        except http_client.BadStatusLine as arg:
            msg = (_("Bad Status line returned: %(arg)s.")
                   % {'arg': arg})
            raise pywbem.cim_http.Error(msg)
        except socket.sslerror as arg:
            msg = (_("SSL error: %(arg)s.")
                   % {'arg': arg})
            raise pywbem.cim_http.Error(msg)
        except socket.error as arg:
            msg = (_("Socket error: %(arg)s.")
                   % {'arg': arg})
            raise pywbem.cim_http.Error(msg)

        break

    return body
