OpenStack Image Management (Glance) Client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
13KB

  1. # Copyright 2014 Red Hat, Inc
  2. # All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import socket
  16. import ssl
  17. import struct
  18. import OpenSSL
  19. from requests import adapters
  20. from requests import compat
  21. try:
  22. from requests.packages.urllib3 import connectionpool
  23. from requests.packages.urllib3 import poolmanager
  24. except ImportError:
  25. from urllib3 import connectionpool
  26. from urllib3 import poolmanager
  27. from oslo_utils import encodeutils
  28. import six
  29. # NOTE(jokke): simplified transition to py3, behaves like py2 xrange
  30. from six.moves import range
  31. from glanceclient.common import utils
  32. try:
  33. from eventlet import patcher
  34. # Handle case where we are running in a monkey patched environment
  35. if patcher.is_monkey_patched('socket'):
  36. from eventlet.green.httplib import HTTPSConnection
  37. from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
  38. from eventlet.greenio import GreenSocket
  39. # TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
  40. GreenSocket.getsockopt = utils.getsockopt
  41. else:
  42. raise ImportError
  43. except ImportError:
  44. try:
  45. from httplib import HTTPSConnection
  46. except ImportError:
  47. from http.client import HTTPSConnection
  48. from OpenSSL.SSL import Connection as Connection
  49. from glanceclient import exc
  50. def verify_callback(host=None):
  51. """
  52. We use a partial around the 'real' verify_callback function
  53. so that we can stash the host value without holding a
  54. reference on the VerifiedHTTPSConnection.
  55. """
  56. def wrapper(connection, x509, errnum,
  57. depth, preverify_ok, host=host):
  58. return do_verify_callback(connection, x509, errnum,
  59. depth, preverify_ok, host=host)
  60. return wrapper
  61. def do_verify_callback(connection, x509, errnum,
  62. depth, preverify_ok, host=None):
  63. """
  64. Verify the server's SSL certificate.
  65. This is a standalone function rather than a method to avoid
  66. issues around closing sockets if a reference is held on
  67. a VerifiedHTTPSConnection by the callback function.
  68. """
  69. if x509.has_expired():
  70. msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
  71. raise exc.SSLCertificateError(msg)
  72. if depth == 0 and preverify_ok:
  73. # We verify that the host matches against the last
  74. # certificate in the chain
  75. return host_matches_cert(host, x509)
  76. else:
  77. # Pass through OpenSSL's default result
  78. return preverify_ok
  79. def host_matches_cert(host, x509):
  80. """
  81. Verify that the x509 certificate we have received
  82. from 'host' correctly identifies the server we are
  83. connecting to, ie that the certificate's Common Name
  84. or a Subject Alternative Name matches 'host'.
  85. """
  86. def check_match(name):
  87. # Directly match the name
  88. if name == host:
  89. return True
  90. # Support single wildcard matching
  91. if name.startswith('*.') and host.find('.') > 0:
  92. if name[2:] == host.split('.', 1)[1]:
  93. return True
  94. common_name = x509.get_subject().commonName
  95. # First see if we can match the CN
  96. if check_match(common_name):
  97. return True
  98. # Also try Subject Alternative Names for a match
  99. san_list = None
  100. for i in range(x509.get_extension_count()):
  101. ext = x509.get_extension(i)
  102. if ext.get_short_name() == b'subjectAltName':
  103. san_list = str(ext)
  104. for san in ''.join(san_list.split()).split(','):
  105. if san.startswith('DNS:'):
  106. if check_match(san.split(':', 1)[1]):
  107. return True
  108. # Server certificate does not match host
  109. msg = ('Host "%s" does not match x509 certificate contents: '
  110. 'CommonName "%s"' % (host, common_name))
  111. if san_list is not None:
  112. msg = msg + ', subjectAltName "%s"' % san_list
  113. raise exc.SSLCertificateError(msg)
  114. def to_bytes(s):
  115. if isinstance(s, six.string_types):
  116. return six.b(s)
  117. else:
  118. return s
  119. class HTTPSAdapter(adapters.HTTPAdapter):
  120. """
  121. This adapter will be used just when
  122. ssl compression should be disabled.
  123. The init method overwrites the default
  124. https pool by setting glanceclient's
  125. one.
  126. """
  127. def __init__(self, *args, **kwargs):
  128. classes_by_scheme = poolmanager.pool_classes_by_scheme
  129. classes_by_scheme["glance+https"] = HTTPSConnectionPool
  130. super(HTTPSAdapter, self).__init__(*args, **kwargs)
  131. def request_url(self, request, proxies):
  132. # NOTE(flaper87): Make sure the url is encoded, otherwise
  133. # python's standard httplib will fail with a TypeError.
  134. url = super(HTTPSAdapter, self).request_url(request, proxies)
  135. return encodeutils.safe_encode(url)
  136. def _create_glance_httpsconnectionpool(self, url):
  137. kw = self.poolmanager.connection_kw
  138. # Parse the url to get the scheme, host, and port
  139. parsed = compat.urlparse(url)
  140. # If there is no port specified, we should use the standard HTTPS port
  141. port = parsed.port or 443
  142. pool = HTTPSConnectionPool(parsed.host, port, **kw)
  143. with self.poolmanager.pools.lock:
  144. self.poolmanager.pools[(parsed.scheme, parsed.host, port)] = pool
  145. return pool
  146. def get_connection(self, url, proxies=None):
  147. try:
  148. return super(HTTPSAdapter, self).get_connection(url, proxies)
  149. except KeyError:
  150. # NOTE(sigamvirus24): This works around modifying a module global
  151. # which fixes bug #1396550
  152. # The scheme is most likely glance+https but check anyway
  153. if not url.startswith('glance+https://'):
  154. raise
  155. return self._create_glance_httpsconnectionpool(url)
  156. def cert_verify(self, conn, url, verify, cert):
  157. super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
  158. conn.ca_certs = verify[0]
  159. conn.insecure = verify[1]
  160. class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
  161. """
  162. HTTPSConnectionPool will be instantiated when a new
  163. connection is requested to the HTTPSAdapter.This
  164. implementation overwrites the _new_conn method and
  165. returns an instances of glanceclient's VerifiedHTTPSConnection
  166. which handles no compression.
  167. ssl_compression is hard-coded to False because this will
  168. be used just when the user sets --no-ssl-compression.
  169. """
  170. scheme = 'glance+https'
  171. def _new_conn(self):
  172. self.num_connections += 1
  173. return VerifiedHTTPSConnection(host=self.host,
  174. port=self.port,
  175. key_file=self.key_file,
  176. cert_file=self.cert_file,
  177. cacert=self.ca_certs,
  178. insecure=self.insecure,
  179. ssl_compression=False)
  180. class OpenSSLConnectionDelegator(object):
  181. """
  182. An OpenSSL.SSL.Connection delegator.
  183. Supplies an additional 'makefile' method which httplib requires
  184. and is not present in OpenSSL.SSL.Connection.
  185. Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
  186. a delegator must be used.
  187. """
  188. def __init__(self, *args, **kwargs):
  189. self.connection = Connection(*args, **kwargs)
  190. def __getattr__(self, name):
  191. return getattr(self.connection, name)
  192. def makefile(self, *args, **kwargs):
  193. return socket._fileobject(self.connection, *args, **kwargs)
  194. class VerifiedHTTPSConnection(HTTPSConnection):
  195. """
  196. Extended HTTPSConnection which uses the OpenSSL library
  197. for enhanced SSL support.
  198. Note: Much of this functionality can eventually be replaced
  199. with native Python 3.3 code.
  200. """
  201. # Restrict the set of client supported cipher suites
  202. CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
  203. 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
  204. 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
  205. def __init__(self, host, port=None, key_file=None, cert_file=None,
  206. cacert=None, timeout=None, insecure=False,
  207. ssl_compression=True):
  208. # List of exceptions reported by Python3 instead of
  209. # SSLConfigurationError
  210. if six.PY3:
  211. excp_lst = (TypeError, FileNotFoundError, ssl.SSLError)
  212. else:
  213. # NOTE(jamespage)
  214. # Accomodate changes in behaviour for pep-0467, introduced
  215. # in python 2.7.9.
  216. # https://github.com/python/peps/blob/master/pep-0476.txt
  217. excp_lst = (TypeError, IOError, ssl.SSLError)
  218. try:
  219. HTTPSConnection.__init__(self, host, port,
  220. key_file=key_file,
  221. cert_file=cert_file)
  222. self.key_file = key_file
  223. self.cert_file = cert_file
  224. self.timeout = timeout
  225. self.insecure = insecure
  226. # NOTE(flaper87): `is_verified` is needed for
  227. # requests' urllib3. If insecure is True then
  228. # the request is not `verified`, hence `not insecure`
  229. self.is_verified = not insecure
  230. self.ssl_compression = ssl_compression
  231. self.cacert = None if cacert is None else str(cacert)
  232. self.set_context()
  233. # ssl exceptions are reported in various form in Python 3
  234. # so to be compatible, we report the same kind as under
  235. # Python2
  236. except excp_lst as e:
  237. raise exc.SSLConfigurationError(str(e))
  238. def set_context(self):
  239. """
  240. Set up the OpenSSL context.
  241. """
  242. self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
  243. self.context.set_cipher_list(self.CIPHERS)
  244. if self.ssl_compression is False:
  245. self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
  246. if self.insecure is not True:
  247. self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
  248. verify_callback(host=self.host))
  249. else:
  250. self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
  251. lambda *args: True)
  252. if self.cert_file:
  253. try:
  254. self.context.use_certificate_file(self.cert_file)
  255. except Exception as e:
  256. msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
  257. raise exc.SSLConfigurationError(msg)
  258. if self.key_file is None:
  259. # We support having key and cert in same file
  260. try:
  261. self.context.use_privatekey_file(self.cert_file)
  262. except Exception as e:
  263. msg = ('No key file specified and unable to load key '
  264. 'from "%s" %s' % (self.cert_file, e))
  265. raise exc.SSLConfigurationError(msg)
  266. if self.key_file:
  267. try:
  268. self.context.use_privatekey_file(self.key_file)
  269. except Exception as e:
  270. msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
  271. raise exc.SSLConfigurationError(msg)
  272. if self.cacert:
  273. try:
  274. self.context.load_verify_locations(to_bytes(self.cacert))
  275. except Exception as e:
  276. msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
  277. raise exc.SSLConfigurationError(msg)
  278. else:
  279. self.context.set_default_verify_paths()
  280. def connect(self):
  281. """
  282. Connect to an SSL port using the OpenSSL library and apply
  283. per-connection parameters.
  284. """
  285. result = socket.getaddrinfo(self.host, self.port, 0,
  286. socket.SOCK_STREAM)
  287. if result:
  288. socket_family = result[0][0]
  289. if socket_family == socket.AF_INET6:
  290. sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  291. else:
  292. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  293. else:
  294. # If due to some reason the address lookup fails - we still connect
  295. # to IPv4 socket. This retains the older behavior.
  296. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  297. if self.timeout is not None:
  298. # '0' microseconds
  299. sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
  300. struct.pack('LL', self.timeout, 0))
  301. self.sock = OpenSSLConnectionDelegator(self.context, sock)
  302. self.sock.connect((self.host, self.port))