1 # Copyright 2014 Red Hat, Inc
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
21 from requests import adapters
22 from requests import compat
24 from requests.packages.urllib3 import connectionpool
26 from urllib3 import connectionpool
28 from oslo_utils import encodeutils
30 # NOTE(jokke): simplified transition to py3, behaves like py2 xrange
31 from six.moves import range
33 from escalatorclient.common import utils
36 from eventlet import patcher
37 # Handle case where we are running in a monkey patched environment
38 if patcher.is_monkey_patched('socket'):
39 from eventlet.green.httplib import HTTPSConnection
40 from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
41 from eventlet.greenio import GreenSocket
42 # TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
43 GreenSocket.getsockopt = utils.getsockopt
48 from httplib import HTTPSConnection
50 from http.client import HTTPSConnection
51 from OpenSSL.SSL import Connection as Connection
54 from escalatorclient import exc
57 def verify_callback(host=None):
59 We use a partial around the 'real' verify_callback function
60 so that we can stash the host value without holding a
61 reference on the VerifiedHTTPSConnection.
63 def wrapper(connection, x509, errnum,
64 depth, preverify_ok, host=host):
65 return do_verify_callback(connection, x509, errnum,
66 depth, preverify_ok, host=host)
70 def do_verify_callback(connection, x509, errnum,
71 depth, preverify_ok, host=None):
73 Verify the server's SSL certificate.
75 This is a standalone function rather than a method to avoid
76 issues around closing sockets if a reference is held on
77 a VerifiedHTTPSConnection by the callback function.
79 if x509.has_expired():
80 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
81 raise exc.SSLCertificateError(msg)
83 if depth == 0 and preverify_ok:
84 # We verify that the host matches against the last
85 # certificate in the chain
86 return host_matches_cert(host, x509)
88 # Pass through OpenSSL's default result
92 def host_matches_cert(host, x509):
94 Verify that the x509 certificate we have received
95 from 'host' correctly identifies the server we are
96 connecting to, ie that the certificate's Common Name
97 or a Subject Alternative Name matches 'host'.
99 def check_match(name):
100 # Directly match the name
104 # Support single wildcard matching
105 if name.startswith('*.') and host.find('.') > 0:
106 if name[2:] == host.split('.', 1)[1]:
109 common_name = x509.get_subject().commonName
111 # First see if we can match the CN
112 if check_match(common_name):
114 # Also try Subject Alternative Names for a match
116 for i in range(x509.get_extension_count()):
117 ext = x509.get_extension(i)
118 if ext.get_short_name() == b'subjectAltName':
120 for san in ''.join(san_list.split()).split(','):
121 if san.startswith('DNS:'):
122 if check_match(san.split(':', 1)[1]):
125 # Server certificate does not match host
126 msg = ('Host "%s" does not match x509 certificate contents: '
127 'CommonName "%s"' % (host, common_name))
128 if san_list is not None:
129 msg = msg + ', subjectAltName "%s"' % san_list
130 raise exc.SSLCertificateError(msg)
134 if isinstance(s, six.string_types):
140 class HTTPSAdapter(adapters.HTTPAdapter):
142 This adapter will be used just when
143 ssl compression should be disabled.
145 The init method overwrites the default
146 https pool by setting escalatorclient's
150 def request_url(self, request, proxies):
151 # NOTE(flaper87): Make sure the url is encoded, otherwise
152 # python's standard httplib will fail with a TypeError.
153 url = super(HTTPSAdapter, self).request_url(request, proxies)
154 return encodeutils.safe_encode(url)
156 def _create_escalator_httpsconnectionpool(self, url):
157 kw = self.poolmanager.connection_kw
158 # Parse the url to get the scheme, host, and port
159 parsed = compat.urlparse(url)
160 # If there is no port specified, we should use the standard HTTPS port
161 port = parsed.port or 443
162 pool = HTTPSConnectionPool(parsed.host, port, **kw)
164 with self.poolmanager.pools.lock:
165 self.poolmanager.pools[(parsed.scheme, parsed.host, port)] = pool
169 def get_connection(self, url, proxies=None):
171 return super(HTTPSAdapter, self).get_connection(url, proxies)
173 # NOTE(sigamvirus24): This works around modifying a module global
174 # which fixes bug #1396550
175 # The scheme is most likely escalator+https but check anyway
176 if not url.startswith('escalator+https://'):
179 return self._create_escalator_httpsconnectionpool(url)
181 def cert_verify(self, conn, url, verify, cert):
182 super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
183 conn.ca_certs = verify[0]
184 conn.insecure = verify[1]
187 class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
189 HTTPSConnectionPool will be instantiated when a new
190 connection is requested to the HTTPSAdapter.This
191 implementation overwrites the _new_conn method and
192 returns an instances of escalatorclient's VerifiedHTTPSConnection
193 which handles no compression.
195 ssl_compression is hard-coded to False because this will
196 be used just when the user sets --no-ssl-compression.
199 scheme = 'escalator+https'
202 self.num_connections += 1
203 return VerifiedHTTPSConnection(host=self.host,
205 key_file=self.key_file,
206 cert_file=self.cert_file,
207 cacert=self.ca_certs,
208 insecure=self.insecure,
209 ssl_compression=False)
212 class OpenSSLConnectionDelegator(object):
214 An OpenSSL.SSL.Connection delegator.
216 Supplies an additional 'makefile' method which httplib requires
217 and is not present in OpenSSL.SSL.Connection.
219 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
220 a delegator must be used.
222 def __init__(self, *args, **kwargs):
223 self.connection = Connection(*args, **kwargs)
225 def __getattr__(self, name):
226 return getattr(self.connection, name)
228 def makefile(self, *args, **kwargs):
229 return socket._fileobject(self.connection, *args, **kwargs)
232 class VerifiedHTTPSConnection(HTTPSConnection):
234 Extended HTTPSConnection which uses the OpenSSL library
235 for enhanced SSL support.
236 Note: Much of this functionality can eventually be replaced
237 with native Python 3.3 code.
239 # Restrict the set of client supported cipher suites
240 CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
241 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
242 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
244 def __init__(self, host, port=None, key_file=None, cert_file=None,
245 cacert=None, timeout=None, insecure=False,
246 ssl_compression=True):
247 # List of exceptions reported by Python3 instead of
248 # SSLConfigurationError
250 excp_lst = (TypeError, IOError, ssl.SSLError)
251 # https.py:250:36: F821 undefined name 'FileNotFoundError'
254 # Accomodate changes in behaviour for pep-0467, introduced
256 # https://github.com/python/peps/blob/master/pep-0476.txt
257 excp_lst = (TypeError, IOError, ssl.SSLError)
259 HTTPSConnection.__init__(self, host, port,
262 self.key_file = key_file
263 self.cert_file = cert_file
264 self.timeout = timeout
265 self.insecure = insecure
266 # NOTE(flaper87): `is_verified` is needed for
267 # requests' urllib3. If insecure is True then
268 # the request is not `verified`, hence `not insecure`
269 self.is_verified = not insecure
270 self.ssl_compression = ssl_compression
271 self.cacert = None if cacert is None else str(cacert)
273 # ssl exceptions are reported in various form in Python 3
274 # so to be compatible, we report the same kind as under
276 except excp_lst as e:
277 raise exc.SSLConfigurationError(str(e))
279 def set_context(self):
281 Set up the OpenSSL context.
283 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
284 self.context.set_cipher_list(self.CIPHERS)
286 if self.ssl_compression is False:
287 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
289 if self.insecure is not True:
290 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
291 verify_callback(host=self.host))
293 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
298 self.context.use_certificate_file(self.cert_file)
299 except Exception as e:
300 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
301 raise exc.SSLConfigurationError(msg)
302 if self.key_file is None:
303 # We support having key and cert in same file
305 self.context.use_privatekey_file(self.cert_file)
306 except Exception as e:
307 msg = ('No key file specified and unable to load key '
308 'from "%s" %s' % (self.cert_file, e))
309 raise exc.SSLConfigurationError(msg)
313 self.context.use_privatekey_file(self.key_file)
314 except Exception as e:
315 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
316 raise exc.SSLConfigurationError(msg)
320 self.context.load_verify_locations(to_bytes(self.cacert))
321 except Exception as e:
322 msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
323 raise exc.SSLConfigurationError(msg)
325 self.context.set_default_verify_paths()
329 Connect to an SSL port using the OpenSSL library and apply
330 per-connection parameters.
332 result = socket.getaddrinfo(self.host, self.port, 0,
335 socket_family = result[0][0]
336 if socket_family == socket.AF_INET6:
337 sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
339 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
341 # If due to some reason the address lookup fails - we still connect
342 # to IPv4 socket. This retains the older behavior.
343 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
344 if self.timeout is not None:
346 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
347 struct.pack('LL', self.timeout, 0))
348 self.sock = OpenSSLConnectionDelegator(self.context, sock)
349 self.sock.connect((self.host, self.port))