blob: dd1448adaa9711fb21e795dd93ec6b33d9f83d53 [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 httplib
Attila Fazekasc7920282013-03-01 13:04:54 +010021import json
Matthew Treinish72ea4422013-02-07 14:42:49 -050022import posixpath
Matthew Treinish6900ba12013-02-19 16:38:01 -050023import re
Matthew Treinish72ea4422013-02-07 14:42:49 -050024import socket
25import StringIO
26import struct
27import urlparse
28
Matthew Treinish96e9e882014-06-09 18:37:19 -040029
30import OpenSSL
31from six import moves
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 Treinishf4a9b0f2013-07-26 16:58:26 -040035from tempest.openstack.common import log as logging
Matthew Treinish72ea4422013-02-07 14:42:49 -050036
37LOG = logging.getLogger(__name__)
38USER_AGENT = 'tempest'
39CHUNKSIZE = 1024 * 64 # 64kB
Matthew Treinish6900ba12013-02-19 16:38:01 -050040TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Matthew Treinish72ea4422013-02-07 14:42:49 -050041
42
43class HTTPClient(object):
44
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000045 def __init__(self, auth_provider, filters, **kwargs):
46 self.auth_provider = auth_provider
47 self.filters = filters
48 self.endpoint = auth_provider.base_url(filters)
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -050049 endpoint_parts = urlparse.urlparse(self.endpoint)
Matthew Treinish72ea4422013-02-07 14:42:49 -050050 self.endpoint_scheme = endpoint_parts.scheme
51 self.endpoint_hostname = endpoint_parts.hostname
52 self.endpoint_port = endpoint_parts.port
53 self.endpoint_path = endpoint_parts.path
54
55 self.connection_class = self.get_connection_class(self.endpoint_scheme)
56 self.connection_kwargs = self.get_connection_kwargs(
57 self.endpoint_scheme, **kwargs)
58
Matthew Treinish72ea4422013-02-07 14:42:49 -050059 @staticmethod
Matthew Treinish72ea4422013-02-07 14:42:49 -050060 def get_connection_class(scheme):
61 if scheme == 'https':
62 return VerifiedHTTPSConnection
63 else:
64 return httplib.HTTPConnection
65
66 @staticmethod
67 def get_connection_kwargs(scheme, **kwargs):
68 _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
69
70 if scheme == 'https':
Joseph Lanouxc9c06be2015-01-21 09:03:30 +000071 _kwargs['ca_certs'] = kwargs.get('ca_certs', None)
Matthew Treinish72ea4422013-02-07 14:42:49 -050072 _kwargs['cert_file'] = kwargs.get('cert_file', None)
73 _kwargs['key_file'] = kwargs.get('key_file', None)
74 _kwargs['insecure'] = kwargs.get('insecure', False)
75 _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
76
77 return _kwargs
78
79 def get_connection(self):
80 _class = self.connection_class
81 try:
82 return _class(self.endpoint_hostname, self.endpoint_port,
83 **self.connection_kwargs)
84 except httplib.InvalidURL:
85 raise exc.EndpointNotFound
86
Matthew Treinish72ea4422013-02-07 14:42:49 -050087 def _http_request(self, url, method, **kwargs):
Attila Fazekasb2902af2013-02-16 16:22:44 +010088 """Send an http request with the specified characteristics.
Matthew Treinish72ea4422013-02-07 14:42:49 -050089
90 Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
91 as setting headers and error handling.
92 """
93 # Copy the kwargs so we can reuse the original in case of redirects
94 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
95 kwargs['headers'].setdefault('User-Agent', USER_AGENT)
Matthew Treinish72ea4422013-02-07 14:42:49 -050096
Matthew Treinish6900ba12013-02-19 16:38:01 -050097 self._log_request(method, url, kwargs['headers'])
98
Matthew Treinish72ea4422013-02-07 14:42:49 -050099 conn = self.get_connection()
100
101 try:
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500102 url_parts = urlparse.urlparse(url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000103 conn_url = posixpath.normpath(url_parts.path)
104 LOG.debug('Actual Path: {path}'.format(path=conn_url))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500105 if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
106 conn.putrequest(method, conn_url)
107 for header, value in kwargs['headers'].items():
108 conn.putheader(header, value)
109 conn.endheaders()
110 chunk = kwargs['body'].read(CHUNKSIZE)
111 # Chunk it, baby...
112 while chunk:
113 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
114 chunk = kwargs['body'].read(CHUNKSIZE)
115 conn.send('0\r\n\r\n')
116 else:
117 conn.request(method, conn_url, **kwargs)
118 resp = conn.getresponse()
119 except socket.gaierror as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400120 message = ("Error finding address for %(url)s: %(e)s" %
121 {'url': url, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100122 raise exc.EndpointNotFound(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500123 except (socket.error, socket.timeout) as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400124 message = ("Error communicating with %(endpoint)s %(e)s" %
125 {'endpoint': self.endpoint, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100126 raise exc.TimeoutException(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500127
128 body_iter = ResponseBodyIterator(resp)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500129 # Read body into string if it isn't obviously image data
130 if resp.getheader('content-type', None) != 'application/octet-stream':
Monty Taylorb2ca5ca2013-04-28 18:00:21 -0700131 body_str = ''.join([body_chunk for body_chunk in body_iter])
Matthew Treinish72ea4422013-02-07 14:42:49 -0500132 body_iter = StringIO.StringIO(body_str)
Matthew Treinish6900ba12013-02-19 16:38:01 -0500133 self._log_response(resp, None)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500134 else:
Matthew Treinish6900ba12013-02-19 16:38:01 -0500135 self._log_response(resp, body_iter)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500136
137 return resp, body_iter
138
Matthew Treinish6900ba12013-02-19 16:38:01 -0500139 def _log_request(self, method, url, headers):
140 LOG.info('Request: ' + method + ' ' + url)
141 if headers:
142 headers_out = headers
143 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
144 token = headers['X-Auth-Token']
145 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
146 headers_out = headers.copy()
147 headers_out['X-Auth-Token'] = "<Token omitted>"
148 LOG.info('Request Headers: ' + str(headers_out))
149
150 def _log_response(self, resp, body):
151 status = str(resp.status)
152 LOG.info("Response Status: " + status)
153 if resp.getheaders():
154 LOG.info('Response Headers: ' + str(resp.getheaders()))
155 if body:
156 str_body = str(body)
157 length = len(body)
158 LOG.info('Response Body: ' + str_body[:2048])
159 if length >= 2048:
160 self.LOG.debug("Large body (%d) md5 summary: %s", length,
161 hashlib.md5(str_body).hexdigest())
162
Matthew Treinish72ea4422013-02-07 14:42:49 -0500163 def json_request(self, method, url, **kwargs):
164 kwargs.setdefault('headers', {})
165 kwargs['headers'].setdefault('Content-Type', 'application/json')
Mauro S. M. Rodrigues5403b792014-05-06 15:24:54 -0400166 if kwargs['headers']['Content-Type'] != 'application/json':
167 msg = "Only application/json content-type is supported."
Masayuki Igawad5f3b6c2015-01-20 14:40:45 +0900168 raise lib_exc.InvalidContentType(msg)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500169
170 if 'body' in kwargs:
171 kwargs['body'] = json.dumps(kwargs['body'])
172
173 resp, body_iter = self._http_request(url, method, **kwargs)
174
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500175 if 'application/json' in resp.getheader('content-type', ''):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500176 body = ''.join([chunk for chunk in body_iter])
177 try:
178 body = json.loads(body)
179 except ValueError:
180 LOG.error('Could not decode response body as JSON')
181 else:
Mauro S. M. Rodrigues5403b792014-05-06 15:24:54 -0400182 msg = "Only json/application content-type is supported."
Masayuki Igawad5f3b6c2015-01-20 14:40:45 +0900183 raise lib_exc.InvalidContentType(msg)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500184
185 return resp, body
186
187 def raw_request(self, method, url, **kwargs):
188 kwargs.setdefault('headers', {})
189 kwargs['headers'].setdefault('Content-Type',
190 'application/octet-stream')
191 if 'body' in kwargs:
192 if (hasattr(kwargs['body'], 'read')
193 and method.lower() in ('post', 'put')):
194 # We use 'Transfer-Encoding: chunked' because
195 # body size may not always be known in advance.
196 kwargs['headers']['Transfer-Encoding'] = 'chunked'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000197
198 # Decorate the request with auth
199 req_url, kwargs['headers'], kwargs['body'] = \
200 self.auth_provider.auth_request(
201 method=method, url=url, headers=kwargs['headers'],
202 body=kwargs.get('body', None), filters=self.filters)
203 return self._http_request(req_url, method, **kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500204
205
206class OpenSSLConnectionDelegator(object):
207 """
208 An OpenSSL.SSL.Connection delegator.
209
210 Supplies an additional 'makefile' method which httplib requires
211 and is not present in OpenSSL.SSL.Connection.
212
213 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
214 a delegator must be used.
215 """
216 def __init__(self, *args, **kwargs):
217 self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
218
219 def __getattr__(self, name):
220 return getattr(self.connection, name)
221
222 def makefile(self, *args, **kwargs):
Matthew Treinishab23e902014-01-27 22:18:15 +0000223 # Ensure the socket is closed when this file is closed
224 kwargs['close'] = True
Matthew Treinish72ea4422013-02-07 14:42:49 -0500225 return socket._fileobject(self.connection, *args, **kwargs)
226
227
228class VerifiedHTTPSConnection(httplib.HTTPSConnection):
229 """
230 Extended HTTPSConnection which uses the OpenSSL library
231 for enhanced SSL support.
232 Note: Much of this functionality can eventually be replaced
233 with native Python 3.3 code.
234 """
235 def __init__(self, host, port=None, key_file=None, cert_file=None,
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000236 ca_certs=None, timeout=None, insecure=False,
Matthew Treinish72ea4422013-02-07 14:42:49 -0500237 ssl_compression=True):
238 httplib.HTTPSConnection.__init__(self, host, port,
239 key_file=key_file,
240 cert_file=cert_file)
241 self.key_file = key_file
242 self.cert_file = cert_file
243 self.timeout = timeout
244 self.insecure = insecure
245 self.ssl_compression = ssl_compression
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000246 self.ca_certs = ca_certs
Matthew Treinish72ea4422013-02-07 14:42:49 -0500247 self.setcontext()
248
249 @staticmethod
250 def host_matches_cert(host, x509):
251 """
252 Verify that the the x509 certificate we have received
253 from 'host' correctly identifies the server we are
254 connecting to, ie that the certificate's Common Name
255 or a Subject Alternative Name matches 'host'.
256 """
257 # First see if we can match the CN
258 if x509.get_subject().commonName == host:
259 return True
260
261 # Also try Subject Alternative Names for a match
262 san_list = None
llg821243b20502014-02-22 10:32:49 +0800263 for i in moves.xrange(x509.get_extension_count()):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500264 ext = x509.get_extension(i)
265 if ext.get_short_name() == 'subjectAltName':
266 san_list = str(ext)
267 for san in ''.join(san_list.split()).split(','):
268 if san == "DNS:%s" % host:
269 return True
270
271 # Server certificate does not match host
272 msg = ('Host "%s" does not match x509 certificate contents: '
273 'CommonName "%s"' % (host, x509.get_subject().commonName))
274 if san_list is not None:
275 msg = msg + ', subjectAltName "%s"' % san_list
276 raise exc.SSLCertificateError(msg)
277
278 def verify_callback(self, connection, x509, errnum,
279 depth, preverify_ok):
280 if x509.has_expired():
281 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
282 raise exc.SSLCertificateError(msg)
283
284 if depth == 0 and preverify_ok is True:
285 # We verify that the host matches against the last
286 # certificate in the chain
287 return self.host_matches_cert(self.host, x509)
288 else:
289 # Pass through OpenSSL's default result
290 return preverify_ok
291
292 def setcontext(self):
293 """
294 Set up the OpenSSL context.
295 """
296 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
297
298 if self.ssl_compression is False:
299 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
300
301 if self.insecure is not True:
302 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
303 self.verify_callback)
304 else:
305 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
306 self.verify_callback)
307
308 if self.cert_file:
309 try:
310 self.context.use_certificate_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200311 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500312 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
313 raise exc.SSLConfigurationError(msg)
314 if self.key_file is None:
315 # We support having key and cert in same file
316 try:
317 self.context.use_privatekey_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200318 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500319 msg = ('No key file specified and unable to load key '
320 'from "%s" %s' % (self.cert_file, e))
321 raise exc.SSLConfigurationError(msg)
322
323 if self.key_file:
324 try:
325 self.context.use_privatekey_file(self.key_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200326 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500327 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
328 raise exc.SSLConfigurationError(msg)
329
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000330 if self.ca_certs:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500331 try:
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000332 self.context.load_verify_locations(self.ca_certs)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200333 except Exception as e:
Joseph Lanouxc9c06be2015-01-21 09:03:30 +0000334 msg = 'Unable to load CA from "%s"' % (self.ca_certs, e)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500335 raise exc.SSLConfigurationError(msg)
336 else:
337 self.context.set_default_verify_paths()
338
339 def connect(self):
340 """
341 Connect to an SSL port using the OpenSSL library and apply
342 per-connection parameters.
343 """
344 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
345 if self.timeout is not None:
346 # '0' microseconds
347 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
348 struct.pack('LL', self.timeout, 0))
349 self.sock = OpenSSLConnectionDelegator(self.context, sock)
350 self.sock.connect((self.host, self.port))
351
Matthew Treinishab23e902014-01-27 22:18:15 +0000352 def close(self):
353 if self.sock:
354 # Remove the reference to the socket but don't close it yet.
355 # Response close will close both socket and associated
356 # file. Closing socket too soon will cause response
357 # reads to fail with socket IO error 'Bad file descriptor'.
358 self.sock = None
Matthew Treinish7741cd62014-01-28 16:10:40 +0000359 httplib.HTTPSConnection.close(self)
Matthew Treinishab23e902014-01-27 22:18:15 +0000360
Matthew Treinish72ea4422013-02-07 14:42:49 -0500361
362class ResponseBodyIterator(object):
363 """A class that acts as an iterator over an HTTP response."""
364
365 def __init__(self, resp):
366 self.resp = resp
367
368 def __iter__(self):
369 while True:
370 yield self.next()
371
372 def next(self):
373 chunk = self.resp.read(CHUNKSIZE)
374 if chunk:
375 return chunk
376 else:
377 raise StopIteration()