blob: 800e977b30c8bb4f2561a9e6644a73ec9b601470 [file] [log] [blame]
ZhiQiang Fan39f97222013-09-20 04:49:44 +08001# Copyright 2012 OpenStack Foundation
Matthew Treinish72ea4422013-02-07 14:42:49 -05002# 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
16# Originally copied from python-glanceclient
17
18import copy
Matthew Treinish6900ba12013-02-19 16:38:01 -050019import hashlib
Matthew Treinish72ea4422013-02-07 14:42:49 -050020import posixpath
Matthew Treinish6900ba12013-02-19 16:38:01 -050021import re
Matthew Treinish72ea4422013-02-07 14:42:49 -050022import socket
Matthew Treinish72ea4422013-02-07 14:42:49 -050023import struct
Matthew Treinish96e9e882014-06-09 18:37:19 -040024
25import OpenSSL
Doug Hellmann583ce2c2015-03-11 14:55:46 +000026from oslo_log import log as logging
Matthew Treinishb0c65f22015-04-23 09:09:41 -040027import six
Matthew Treinish96e9e882014-06-09 18:37:19 -040028from six import moves
Matthew Treinish6421af82015-04-23 09:47:50 -040029from six.moves import http_client as httplib
Matthew Treinishf077dd22015-04-23 09:37:41 -040030from six.moves.urllib import parse as urlparse
Matthew Treinish96e9e882014-06-09 18:37:19 -040031
Matthew Treinish72ea4422013-02-07 14:42:49 -050032from tempest import exceptions as exc
Matthew Treinish72ea4422013-02-07 14:42:49 -050033
34LOG = logging.getLogger(__name__)
35USER_AGENT = 'tempest'
36CHUNKSIZE = 1024 * 64 # 64kB
Matthew Treinish6900ba12013-02-19 16:38:01 -050037TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Matthew Treinish72ea4422013-02-07 14:42:49 -050038
39
40class HTTPClient(object):
41
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000042 def __init__(self, auth_provider, filters, **kwargs):
43 self.auth_provider = auth_provider
44 self.filters = filters
45 self.endpoint = auth_provider.base_url(filters)
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -050046 endpoint_parts = urlparse.urlparse(self.endpoint)
Matthew Treinish72ea4422013-02-07 14:42:49 -050047 self.endpoint_scheme = endpoint_parts.scheme
48 self.endpoint_hostname = endpoint_parts.hostname
49 self.endpoint_port = endpoint_parts.port
50 self.endpoint_path = endpoint_parts.path
51
Ken'ichi Ohmichi8f6cf5e2015-11-30 12:24:31 +000052 self.connection_class = self._get_connection_class(
53 self.endpoint_scheme)
54 self.connection_kwargs = self._get_connection_kwargs(
Matthew Treinish72ea4422013-02-07 14:42:49 -050055 self.endpoint_scheme, **kwargs)
56
Matthew Treinish72ea4422013-02-07 14:42:49 -050057 @staticmethod
Ken'ichi Ohmichi8f6cf5e2015-11-30 12:24:31 +000058 def _get_connection_class(scheme):
Matthew Treinish72ea4422013-02-07 14:42:49 -050059 if scheme == 'https':
60 return VerifiedHTTPSConnection
61 else:
62 return httplib.HTTPConnection
63
64 @staticmethod
Ken'ichi Ohmichi8f6cf5e2015-11-30 12:24:31 +000065 def _get_connection_kwargs(scheme, **kwargs):
Matthew Treinish72ea4422013-02-07 14:42:49 -050066 _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
67
68 if scheme == 'https':
Joseph Lanouxc9c06be2015-01-21 09:03:30 +000069 _kwargs['ca_certs'] = kwargs.get('ca_certs', None)
Matthew Treinish72ea4422013-02-07 14:42:49 -050070 _kwargs['cert_file'] = kwargs.get('cert_file', None)
71 _kwargs['key_file'] = kwargs.get('key_file', None)
72 _kwargs['insecure'] = kwargs.get('insecure', False)
73 _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
74
75 return _kwargs
76
Ken'ichi Ohmichi8f6cf5e2015-11-30 12:24:31 +000077 def _get_connection(self):
Matthew Treinish72ea4422013-02-07 14:42:49 -050078 _class = self.connection_class
79 try:
80 return _class(self.endpoint_hostname, self.endpoint_port,
81 **self.connection_kwargs)
82 except httplib.InvalidURL:
83 raise exc.EndpointNotFound
84
Matthew Treinish72ea4422013-02-07 14:42:49 -050085 def _http_request(self, url, method, **kwargs):
Attila Fazekasb2902af2013-02-16 16:22:44 +010086 """Send an http request with the specified characteristics.
Matthew Treinish72ea4422013-02-07 14:42:49 -050087
88 Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
89 as setting headers and error handling.
90 """
91 # Copy the kwargs so we can reuse the original in case of redirects
92 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
93 kwargs['headers'].setdefault('User-Agent', USER_AGENT)
Matthew Treinish72ea4422013-02-07 14:42:49 -050094
Matthew Treinish6900ba12013-02-19 16:38:01 -050095 self._log_request(method, url, kwargs['headers'])
96
Ken'ichi Ohmichi8f6cf5e2015-11-30 12:24:31 +000097 conn = self._get_connection()
Matthew Treinish72ea4422013-02-07 14:42:49 -050098
99 try:
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500100 url_parts = urlparse.urlparse(url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000101 conn_url = posixpath.normpath(url_parts.path)
102 LOG.debug('Actual Path: {path}'.format(path=conn_url))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500103 if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
104 conn.putrequest(method, conn_url)
105 for header, value in kwargs['headers'].items():
106 conn.putheader(header, value)
107 conn.endheaders()
108 chunk = kwargs['body'].read(CHUNKSIZE)
109 # Chunk it, baby...
110 while chunk:
111 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
112 chunk = kwargs['body'].read(CHUNKSIZE)
113 conn.send('0\r\n\r\n')
114 else:
115 conn.request(method, conn_url, **kwargs)
116 resp = conn.getresponse()
117 except socket.gaierror as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400118 message = ("Error finding address for %(url)s: %(e)s" %
119 {'url': url, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100120 raise exc.EndpointNotFound(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500121 except (socket.error, socket.timeout) as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400122 message = ("Error communicating with %(endpoint)s %(e)s" %
123 {'endpoint': self.endpoint, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100124 raise exc.TimeoutException(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500125
126 body_iter = ResponseBodyIterator(resp)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500127 # Read body into string if it isn't obviously image data
128 if resp.getheader('content-type', None) != 'application/octet-stream':
Monty Taylorb2ca5ca2013-04-28 18:00:21 -0700129 body_str = ''.join([body_chunk for body_chunk in body_iter])
Matthew Treinishb0c65f22015-04-23 09:09:41 -0400130 body_iter = six.StringIO(body_str)
Matthew Treinish6900ba12013-02-19 16:38:01 -0500131 self._log_response(resp, None)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500132 else:
Matthew Treinish6900ba12013-02-19 16:38:01 -0500133 self._log_response(resp, body_iter)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500134
135 return resp, body_iter
136
Matthew Treinish6900ba12013-02-19 16:38:01 -0500137 def _log_request(self, method, url, headers):
138 LOG.info('Request: ' + method + ' ' + url)
139 if headers:
140 headers_out = headers
141 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
142 token = headers['X-Auth-Token']
143 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
144 headers_out = headers.copy()
145 headers_out['X-Auth-Token'] = "<Token omitted>"
146 LOG.info('Request Headers: ' + str(headers_out))
147
148 def _log_response(self, resp, body):
149 status = str(resp.status)
150 LOG.info("Response Status: " + status)
151 if resp.getheaders():
152 LOG.info('Response Headers: ' + str(resp.getheaders()))
153 if body:
154 str_body = str(body)
155 length = len(body)
156 LOG.info('Response Body: ' + str_body[:2048])
157 if length >= 2048:
158 self.LOG.debug("Large body (%d) md5 summary: %s", length,
159 hashlib.md5(str_body).hexdigest())
160
Matthew Treinish72ea4422013-02-07 14:42:49 -0500161 def raw_request(self, method, url, **kwargs):
162 kwargs.setdefault('headers', {})
163 kwargs['headers'].setdefault('Content-Type',
164 'application/octet-stream')
165 if 'body' in kwargs:
166 if (hasattr(kwargs['body'], 'read')
167 and method.lower() in ('post', 'put')):
168 # We use 'Transfer-Encoding: chunked' because
169 # body size may not always be known in advance.
170 kwargs['headers']['Transfer-Encoding'] = 'chunked'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000171
172 # Decorate the request with auth
173 req_url, kwargs['headers'], kwargs['body'] = \
174 self.auth_provider.auth_request(
175 method=method, url=url, headers=kwargs['headers'],
176 body=kwargs.get('body', None), filters=self.filters)
177 return self._http_request(req_url, method, **kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500178
179
180class OpenSSLConnectionDelegator(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000181 """An OpenSSL.SSL.Connection delegator.
Matthew Treinish72ea4422013-02-07 14:42:49 -0500182
183 Supplies an additional 'makefile' method which httplib requires
184 and is not present in OpenSSL.SSL.Connection.
185
186 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
187 a delegator must be used.
188 """
189 def __init__(self, *args, **kwargs):
190 self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
191
192 def __getattr__(self, name):
193 return getattr(self.connection, name)
194
195 def makefile(self, *args, **kwargs):
Matthew Treinishab23e902014-01-27 22:18:15 +0000196 # Ensure the socket is closed when this file is closed
197 kwargs['close'] = True
Matthew Treinish72ea4422013-02-07 14:42:49 -0500198 return socket._fileobject(self.connection, *args, **kwargs)
199
200
201class VerifiedHTTPSConnection(httplib.HTTPSConnection):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000202 """Extended HTTPSConnection which uses OpenSSL library for enhanced SSL
203
Matthew Treinish72ea4422013-02-07 14:42:49 -0500204 Note: Much of this functionality can eventually be replaced
205 with native Python 3.3 code.
206 """
207 def __init__(self, host, port=None, key_file=None, cert_file=None,
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000208 ca_certs=None, timeout=None, insecure=False,
Matthew Treinish72ea4422013-02-07 14:42:49 -0500209 ssl_compression=True):
210 httplib.HTTPSConnection.__init__(self, host, port,
211 key_file=key_file,
212 cert_file=cert_file)
213 self.key_file = key_file
214 self.cert_file = cert_file
215 self.timeout = timeout
216 self.insecure = insecure
217 self.ssl_compression = ssl_compression
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000218 self.ca_certs = ca_certs
Matthew Treinish72ea4422013-02-07 14:42:49 -0500219 self.setcontext()
220
221 @staticmethod
222 def host_matches_cert(host, x509):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000223 """Verify that the x509 certificate we have received from 'host'
224
225 Identifies the server we are connecting to, ie that the certificate's
226 Common Name or a Subject Alternative Name matches 'host'.
Matthew Treinish72ea4422013-02-07 14:42:49 -0500227 """
228 # First see if we can match the CN
229 if x509.get_subject().commonName == host:
230 return True
231
232 # Also try Subject Alternative Names for a match
233 san_list = None
llg821243b20502014-02-22 10:32:49 +0800234 for i in moves.xrange(x509.get_extension_count()):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500235 ext = x509.get_extension(i)
236 if ext.get_short_name() == 'subjectAltName':
237 san_list = str(ext)
238 for san in ''.join(san_list.split()).split(','):
239 if san == "DNS:%s" % host:
240 return True
241
242 # Server certificate does not match host
243 msg = ('Host "%s" does not match x509 certificate contents: '
244 'CommonName "%s"' % (host, x509.get_subject().commonName))
245 if san_list is not None:
246 msg = msg + ', subjectAltName "%s"' % san_list
247 raise exc.SSLCertificateError(msg)
248
249 def verify_callback(self, connection, x509, errnum,
250 depth, preverify_ok):
251 if x509.has_expired():
252 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
253 raise exc.SSLCertificateError(msg)
254
255 if depth == 0 and preverify_ok is True:
256 # We verify that the host matches against the last
257 # certificate in the chain
258 return self.host_matches_cert(self.host, x509)
259 else:
260 # Pass through OpenSSL's default result
261 return preverify_ok
262
263 def setcontext(self):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000264 """Set up the OpenSSL context."""
Matthew Treinish72ea4422013-02-07 14:42:49 -0500265 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
266
267 if self.ssl_compression is False:
268 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
269
270 if self.insecure is not True:
271 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
272 self.verify_callback)
273 else:
274 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
275 self.verify_callback)
276
277 if self.cert_file:
278 try:
279 self.context.use_certificate_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200280 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500281 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
282 raise exc.SSLConfigurationError(msg)
283 if self.key_file is None:
284 # We support having key and cert in same file
285 try:
286 self.context.use_privatekey_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200287 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500288 msg = ('No key file specified and unable to load key '
289 'from "%s" %s' % (self.cert_file, e))
290 raise exc.SSLConfigurationError(msg)
291
292 if self.key_file:
293 try:
294 self.context.use_privatekey_file(self.key_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200295 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500296 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
297 raise exc.SSLConfigurationError(msg)
298
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000299 if self.ca_certs:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500300 try:
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000301 self.context.load_verify_locations(self.ca_certs)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200302 except Exception as e:
PranaliDeore456d1b62015-08-10 05:16:53 -0700303 msg = 'Unable to load CA from "%s" %s' % (self.ca_certs, e)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500304 raise exc.SSLConfigurationError(msg)
305 else:
306 self.context.set_default_verify_paths()
307
308 def connect(self):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000309 """Connect to SSL port and apply per-connection parameters."""
Matthew Treinish72ea4422013-02-07 14:42:49 -0500310 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
311 if self.timeout is not None:
312 # '0' microseconds
313 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
314 struct.pack('LL', self.timeout, 0))
315 self.sock = OpenSSLConnectionDelegator(self.context, sock)
316 self.sock.connect((self.host, self.port))
317
Matthew Treinishab23e902014-01-27 22:18:15 +0000318 def close(self):
319 if self.sock:
320 # Remove the reference to the socket but don't close it yet.
321 # Response close will close both socket and associated
322 # file. Closing socket too soon will cause response
323 # reads to fail with socket IO error 'Bad file descriptor'.
324 self.sock = None
Matthew Treinish7741cd62014-01-28 16:10:40 +0000325 httplib.HTTPSConnection.close(self)
Matthew Treinishab23e902014-01-27 22:18:15 +0000326
Matthew Treinish72ea4422013-02-07 14:42:49 -0500327
328class ResponseBodyIterator(object):
329 """A class that acts as an iterator over an HTTP response."""
330
331 def __init__(self, resp):
332 self.resp = resp
333
334 def __iter__(self):
335 while True:
336 yield self.next()
337
338 def next(self):
339 chunk = self.resp.read(CHUNKSIZE)
340 if chunk:
341 return chunk
342 else:
343 raise StopIteration()