cinder/cinder/volume/drivers/emc/emc_vmax_https.py

347 lines
12 KiB
Python

# 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
import OpenSSL
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 patcher.is_monkey_patched('socket'):
from eventlet.green.OpenSSL import SSL
else:
raise ImportError
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.error as arg:
msg = (_("Socket error:: %(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)
break
return body