blob: e5431a06a946351c678f5954a906d12a71647168 [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 Treinish21905512015-07-13 10:33:35 -040027from oslo_serialization import jsonutils as json
Matthew Treinishb0c65f22015-04-23 09:09:41 -040028import six
Matthew Treinish96e9e882014-06-09 18:37:19 -040029from six import moves
Matthew Treinish6421af82015-04-23 09:47:50 -040030from six.moves import http_client as httplib
Matthew Treinishf077dd22015-04-23 09:37:41 -040031from six.moves.urllib import parse as urlparse
Masayuki Igawad5f3b6c2015-01-20 14:40:45 +090032from tempest_lib import exceptions as lib_exc
Matthew Treinish96e9e882014-06-09 18:37:19 -040033
Matthew Treinish72ea4422013-02-07 14:42:49 -050034from tempest import exceptions as exc
Matthew Treinish72ea4422013-02-07 14:42:49 -050035
36LOG = logging.getLogger(__name__)
37USER_AGENT = 'tempest'
38CHUNKSIZE = 1024 * 64 # 64kB
Matthew Treinish6900ba12013-02-19 16:38:01 -050039TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Matthew Treinish72ea4422013-02-07 14:42:49 -050040
41
42class HTTPClient(object):
43
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000044 def __init__(self, auth_provider, filters, **kwargs):
45 self.auth_provider = auth_provider
46 self.filters = filters
47 self.endpoint = auth_provider.base_url(filters)
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -050048 endpoint_parts = urlparse.urlparse(self.endpoint)
Matthew Treinish72ea4422013-02-07 14:42:49 -050049 self.endpoint_scheme = endpoint_parts.scheme
50 self.endpoint_hostname = endpoint_parts.hostname
51 self.endpoint_port = endpoint_parts.port
52 self.endpoint_path = endpoint_parts.path
53
54 self.connection_class = self.get_connection_class(self.endpoint_scheme)
55 self.connection_kwargs = self.get_connection_kwargs(
56 self.endpoint_scheme, **kwargs)
57
Matthew Treinish72ea4422013-02-07 14:42:49 -050058 @staticmethod
Matthew Treinish72ea4422013-02-07 14:42:49 -050059 def get_connection_class(scheme):
60 if scheme == 'https':
61 return VerifiedHTTPSConnection
62 else:
63 return httplib.HTTPConnection
64
65 @staticmethod
66 def get_connection_kwargs(scheme, **kwargs):
67 _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
68
69 if scheme == 'https':
Joseph Lanouxc9c06be2015-01-21 09:03:30 +000070 _kwargs['ca_certs'] = kwargs.get('ca_certs', None)
Matthew Treinish72ea4422013-02-07 14:42:49 -050071 _kwargs['cert_file'] = kwargs.get('cert_file', None)
72 _kwargs['key_file'] = kwargs.get('key_file', None)
73 _kwargs['insecure'] = kwargs.get('insecure', False)
74 _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
75
76 return _kwargs
77
78 def get_connection(self):
79 _class = self.connection_class
80 try:
81 return _class(self.endpoint_hostname, self.endpoint_port,
82 **self.connection_kwargs)
83 except httplib.InvalidURL:
84 raise exc.EndpointNotFound
85
Matthew Treinish72ea4422013-02-07 14:42:49 -050086 def _http_request(self, url, method, **kwargs):
Attila Fazekasb2902af2013-02-16 16:22:44 +010087 """Send an http request with the specified characteristics.
Matthew Treinish72ea4422013-02-07 14:42:49 -050088
89 Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
90 as setting headers and error handling.
91 """
92 # Copy the kwargs so we can reuse the original in case of redirects
93 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
94 kwargs['headers'].setdefault('User-Agent', USER_AGENT)
Matthew Treinish72ea4422013-02-07 14:42:49 -050095
Matthew Treinish6900ba12013-02-19 16:38:01 -050096 self._log_request(method, url, kwargs['headers'])
97
Matthew Treinish72ea4422013-02-07 14:42:49 -050098 conn = self.get_connection()
99
100 try:
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500101 url_parts = urlparse.urlparse(url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000102 conn_url = posixpath.normpath(url_parts.path)
103 LOG.debug('Actual Path: {path}'.format(path=conn_url))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500104 if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
105 conn.putrequest(method, conn_url)
106 for header, value in kwargs['headers'].items():
107 conn.putheader(header, value)
108 conn.endheaders()
109 chunk = kwargs['body'].read(CHUNKSIZE)
110 # Chunk it, baby...
111 while chunk:
112 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
113 chunk = kwargs['body'].read(CHUNKSIZE)
114 conn.send('0\r\n\r\n')
115 else:
116 conn.request(method, conn_url, **kwargs)
117 resp = conn.getresponse()
118 except socket.gaierror as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400119 message = ("Error finding address for %(url)s: %(e)s" %
120 {'url': url, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100121 raise exc.EndpointNotFound(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500122 except (socket.error, socket.timeout) as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400123 message = ("Error communicating with %(endpoint)s %(e)s" %
124 {'endpoint': self.endpoint, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100125 raise exc.TimeoutException(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500126
127 body_iter = ResponseBodyIterator(resp)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500128 # Read body into string if it isn't obviously image data
129 if resp.getheader('content-type', None) != 'application/octet-stream':
Monty Taylorb2ca5ca2013-04-28 18:00:21 -0700130 body_str = ''.join([body_chunk for body_chunk in body_iter])
Matthew Treinishb0c65f22015-04-23 09:09:41 -0400131 body_iter = six.StringIO(body_str)
Matthew Treinish6900ba12013-02-19 16:38:01 -0500132 self._log_response(resp, None)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500133 else:
Matthew Treinish6900ba12013-02-19 16:38:01 -0500134 self._log_response(resp, body_iter)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500135
136 return resp, body_iter
137
Matthew Treinish6900ba12013-02-19 16:38:01 -0500138 def _log_request(self, method, url, headers):
139 LOG.info('Request: ' + method + ' ' + url)
140 if headers:
141 headers_out = headers
142 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
143 token = headers['X-Auth-Token']
144 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
145 headers_out = headers.copy()
146 headers_out['X-Auth-Token'] = "<Token omitted>"
147 LOG.info('Request Headers: ' + str(headers_out))
148
149 def _log_response(self, resp, body):
150 status = str(resp.status)
151 LOG.info("Response Status: " + status)
152 if resp.getheaders():
153 LOG.info('Response Headers: ' + str(resp.getheaders()))
154 if body:
155 str_body = str(body)
156 length = len(body)
157 LOG.info('Response Body: ' + str_body[:2048])
158 if length >= 2048:
159 self.LOG.debug("Large body (%d) md5 summary: %s", length,
160 hashlib.md5(str_body).hexdigest())
161
Matthew Treinish72ea4422013-02-07 14:42:49 -0500162 def json_request(self, method, url, **kwargs):
163 kwargs.setdefault('headers', {})
164 kwargs['headers'].setdefault('Content-Type', 'application/json')
Mauro S. M. Rodrigues5403b792014-05-06 15:24:54 -0400165 if kwargs['headers']['Content-Type'] != 'application/json':
166 msg = "Only application/json content-type is supported."
Masayuki Igawad5f3b6c2015-01-20 14:40:45 +0900167 raise lib_exc.InvalidContentType(msg)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500168
169 if 'body' in kwargs:
170 kwargs['body'] = json.dumps(kwargs['body'])
171
172 resp, body_iter = self._http_request(url, method, **kwargs)
173
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500174 if 'application/json' in resp.getheader('content-type', ''):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500175 body = ''.join([chunk for chunk in body_iter])
176 try:
177 body = json.loads(body)
178 except ValueError:
179 LOG.error('Could not decode response body as JSON')
180 else:
Mauro S. M. Rodrigues5403b792014-05-06 15:24:54 -0400181 msg = "Only json/application content-type is supported."
Masayuki Igawad5f3b6c2015-01-20 14:40:45 +0900182 raise lib_exc.InvalidContentType(msg)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500183
184 return resp, body
185
186 def raw_request(self, method, url, **kwargs):
187 kwargs.setdefault('headers', {})
188 kwargs['headers'].setdefault('Content-Type',
189 'application/octet-stream')
190 if 'body' in kwargs:
191 if (hasattr(kwargs['body'], 'read')
192 and method.lower() in ('post', 'put')):
193 # We use 'Transfer-Encoding: chunked' because
194 # body size may not always be known in advance.
195 kwargs['headers']['Transfer-Encoding'] = 'chunked'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000196
197 # Decorate the request with auth
198 req_url, kwargs['headers'], kwargs['body'] = \
199 self.auth_provider.auth_request(
200 method=method, url=url, headers=kwargs['headers'],
201 body=kwargs.get('body', None), filters=self.filters)
202 return self._http_request(req_url, method, **kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500203
204
205class OpenSSLConnectionDelegator(object):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000206 """An OpenSSL.SSL.Connection delegator.
Matthew Treinish72ea4422013-02-07 14:42:49 -0500207
208 Supplies an additional 'makefile' method which httplib requires
209 and is not present in OpenSSL.SSL.Connection.
210
211 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
212 a delegator must be used.
213 """
214 def __init__(self, *args, **kwargs):
215 self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
216
217 def __getattr__(self, name):
218 return getattr(self.connection, name)
219
220 def makefile(self, *args, **kwargs):
Matthew Treinishab23e902014-01-27 22:18:15 +0000221 # Ensure the socket is closed when this file is closed
222 kwargs['close'] = True
Matthew Treinish72ea4422013-02-07 14:42:49 -0500223 return socket._fileobject(self.connection, *args, **kwargs)
224
225
226class VerifiedHTTPSConnection(httplib.HTTPSConnection):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000227 """Extended HTTPSConnection which uses OpenSSL library for enhanced SSL
228
Matthew Treinish72ea4422013-02-07 14:42:49 -0500229 Note: Much of this functionality can eventually be replaced
230 with native Python 3.3 code.
231 """
232 def __init__(self, host, port=None, key_file=None, cert_file=None,
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000233 ca_certs=None, timeout=None, insecure=False,
Matthew Treinish72ea4422013-02-07 14:42:49 -0500234 ssl_compression=True):
235 httplib.HTTPSConnection.__init__(self, host, port,
236 key_file=key_file,
237 cert_file=cert_file)
238 self.key_file = key_file
239 self.cert_file = cert_file
240 self.timeout = timeout
241 self.insecure = insecure
242 self.ssl_compression = ssl_compression
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000243 self.ca_certs = ca_certs
Matthew Treinish72ea4422013-02-07 14:42:49 -0500244 self.setcontext()
245
246 @staticmethod
247 def host_matches_cert(host, x509):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000248 """Verify that the x509 certificate we have received from 'host'
249
250 Identifies the server we are connecting to, ie that the certificate's
251 Common Name or a Subject Alternative Name matches 'host'.
Matthew Treinish72ea4422013-02-07 14:42:49 -0500252 """
253 # First see if we can match the CN
254 if x509.get_subject().commonName == host:
255 return True
256
257 # Also try Subject Alternative Names for a match
258 san_list = None
llg821243b20502014-02-22 10:32:49 +0800259 for i in moves.xrange(x509.get_extension_count()):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500260 ext = x509.get_extension(i)
261 if ext.get_short_name() == 'subjectAltName':
262 san_list = str(ext)
263 for san in ''.join(san_list.split()).split(','):
264 if san == "DNS:%s" % host:
265 return True
266
267 # Server certificate does not match host
268 msg = ('Host "%s" does not match x509 certificate contents: '
269 'CommonName "%s"' % (host, x509.get_subject().commonName))
270 if san_list is not None:
271 msg = msg + ', subjectAltName "%s"' % san_list
272 raise exc.SSLCertificateError(msg)
273
274 def verify_callback(self, connection, x509, errnum,
275 depth, preverify_ok):
276 if x509.has_expired():
277 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
278 raise exc.SSLCertificateError(msg)
279
280 if depth == 0 and preverify_ok is True:
281 # We verify that the host matches against the last
282 # certificate in the chain
283 return self.host_matches_cert(self.host, x509)
284 else:
285 # Pass through OpenSSL's default result
286 return preverify_ok
287
288 def setcontext(self):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000289 """Set up the OpenSSL context."""
Matthew Treinish72ea4422013-02-07 14:42:49 -0500290 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
291
292 if self.ssl_compression is False:
293 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
294
295 if self.insecure is not True:
296 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
297 self.verify_callback)
298 else:
299 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
300 self.verify_callback)
301
302 if self.cert_file:
303 try:
304 self.context.use_certificate_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200305 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500306 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
307 raise exc.SSLConfigurationError(msg)
308 if self.key_file is None:
309 # We support having key and cert in same file
310 try:
311 self.context.use_privatekey_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200312 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500313 msg = ('No key file specified and unable to load key '
314 'from "%s" %s' % (self.cert_file, e))
315 raise exc.SSLConfigurationError(msg)
316
317 if self.key_file:
318 try:
319 self.context.use_privatekey_file(self.key_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200320 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500321 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
322 raise exc.SSLConfigurationError(msg)
323
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000324 if self.ca_certs:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500325 try:
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000326 self.context.load_verify_locations(self.ca_certs)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200327 except Exception as e:
PranaliDeore456d1b62015-08-10 05:16:53 -0700328 msg = 'Unable to load CA from "%s" %s' % (self.ca_certs, e)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500329 raise exc.SSLConfigurationError(msg)
330 else:
331 self.context.set_default_verify_paths()
332
333 def connect(self):
Ken'ichi Ohmichicb67d2d2015-11-19 08:23:22 +0000334 """Connect to SSL port and apply per-connection parameters."""
Matthew Treinish72ea4422013-02-07 14:42:49 -0500335 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
336 if self.timeout is not None:
337 # '0' microseconds
338 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
339 struct.pack('LL', self.timeout, 0))
340 self.sock = OpenSSLConnectionDelegator(self.context, sock)
341 self.sock.connect((self.host, self.port))
342
Matthew Treinishab23e902014-01-27 22:18:15 +0000343 def close(self):
344 if self.sock:
345 # Remove the reference to the socket but don't close it yet.
346 # Response close will close both socket and associated
347 # file. Closing socket too soon will cause response
348 # reads to fail with socket IO error 'Bad file descriptor'.
349 self.sock = None
Matthew Treinish7741cd62014-01-28 16:10:40 +0000350 httplib.HTTPSConnection.close(self)
Matthew Treinishab23e902014-01-27 22:18:15 +0000351
Matthew Treinish72ea4422013-02-07 14:42:49 -0500352
353class ResponseBodyIterator(object):
354 """A class that acts as an iterator over an HTTP response."""
355
356 def __init__(self, resp):
357 self.resp = resp
358
359 def __iter__(self):
360 while True:
361 yield self.next()
362
363 def next(self):
364 chunk = self.resp.read(CHUNKSIZE)
365 if chunk:
366 return chunk
367 else:
368 raise StopIteration()