blob: 4503f135b823c4d7cf95c2d2553c464d013284c2 [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 Treinish72ea4422013-02-07 14:42:49 -050029
30# Python 2.5 compat fix
31if not hasattr(urlparse, 'parse_qsl'):
32 import cgi
33 urlparse.parse_qsl = cgi.parse_qsl
34
35import OpenSSL
36
37from tempest import exceptions as exc
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040038from tempest.openstack.common import log as logging
Matthew Treinish72ea4422013-02-07 14:42:49 -050039
40LOG = logging.getLogger(__name__)
41USER_AGENT = 'tempest'
42CHUNKSIZE = 1024 * 64 # 64kB
Matthew Treinish6900ba12013-02-19 16:38:01 -050043TOKEN_CHARS_RE = re.compile('^[-A-Za-z0-9+/=]*$')
Matthew Treinish72ea4422013-02-07 14:42:49 -050044
45
46class HTTPClient(object):
47
Andrea Frittoli8bbdb162014-01-06 11:06:13 +000048 def __init__(self, auth_provider, filters, **kwargs):
49 self.auth_provider = auth_provider
50 self.filters = filters
51 self.endpoint = auth_provider.base_url(filters)
Matthew Treinish72ea4422013-02-07 14:42:49 -050052 endpoint_parts = self.parse_endpoint(self.endpoint)
53 self.endpoint_scheme = endpoint_parts.scheme
54 self.endpoint_hostname = endpoint_parts.hostname
55 self.endpoint_port = endpoint_parts.port
56 self.endpoint_path = endpoint_parts.path
57
58 self.connection_class = self.get_connection_class(self.endpoint_scheme)
59 self.connection_kwargs = self.get_connection_kwargs(
60 self.endpoint_scheme, **kwargs)
61
Matthew Treinish72ea4422013-02-07 14:42:49 -050062 @staticmethod
63 def parse_endpoint(endpoint):
64 return urlparse.urlparse(endpoint)
65
66 @staticmethod
67 def get_connection_class(scheme):
68 if scheme == 'https':
69 return VerifiedHTTPSConnection
70 else:
71 return httplib.HTTPConnection
72
73 @staticmethod
74 def get_connection_kwargs(scheme, **kwargs):
75 _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
76
77 if scheme == 'https':
78 _kwargs['cacert'] = kwargs.get('cacert', None)
79 _kwargs['cert_file'] = kwargs.get('cert_file', None)
80 _kwargs['key_file'] = kwargs.get('key_file', None)
81 _kwargs['insecure'] = kwargs.get('insecure', False)
82 _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
83
84 return _kwargs
85
86 def get_connection(self):
87 _class = self.connection_class
88 try:
89 return _class(self.endpoint_hostname, self.endpoint_port,
90 **self.connection_kwargs)
91 except httplib.InvalidURL:
92 raise exc.EndpointNotFound
93
Matthew Treinish72ea4422013-02-07 14:42:49 -050094 def _http_request(self, url, method, **kwargs):
Attila Fazekasb2902af2013-02-16 16:22:44 +010095 """Send an http request with the specified characteristics.
Matthew Treinish72ea4422013-02-07 14:42:49 -050096
97 Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
98 as setting headers and error handling.
99 """
100 # Copy the kwargs so we can reuse the original in case of redirects
101 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
102 kwargs['headers'].setdefault('User-Agent', USER_AGENT)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500103
Matthew Treinish6900ba12013-02-19 16:38:01 -0500104 self._log_request(method, url, kwargs['headers'])
105
Matthew Treinish72ea4422013-02-07 14:42:49 -0500106 conn = self.get_connection()
107
108 try:
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000109 url_parts = self.parse_endpoint(url)
110 conn_url = posixpath.normpath(url_parts.path)
111 LOG.debug('Actual Path: {path}'.format(path=conn_url))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500112 if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
113 conn.putrequest(method, conn_url)
114 for header, value in kwargs['headers'].items():
115 conn.putheader(header, value)
116 conn.endheaders()
117 chunk = kwargs['body'].read(CHUNKSIZE)
118 # Chunk it, baby...
119 while chunk:
120 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
121 chunk = kwargs['body'].read(CHUNKSIZE)
122 conn.send('0\r\n\r\n')
123 else:
124 conn.request(method, conn_url, **kwargs)
125 resp = conn.getresponse()
126 except socket.gaierror as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400127 message = ("Error finding address for %(url)s: %(e)s" %
128 {'url': url, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100129 raise exc.EndpointNotFound(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500130 except (socket.error, socket.timeout) as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400131 message = ("Error communicating with %(endpoint)s %(e)s" %
132 {'endpoint': self.endpoint, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100133 raise exc.TimeoutException(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500134
135 body_iter = ResponseBodyIterator(resp)
136
137 # Read body into string if it isn't obviously image data
138 if resp.getheader('content-type', None) != 'application/octet-stream':
Monty Taylorb2ca5ca2013-04-28 18:00:21 -0700139 body_str = ''.join([body_chunk for body_chunk in body_iter])
Matthew Treinish72ea4422013-02-07 14:42:49 -0500140 body_iter = StringIO.StringIO(body_str)
Matthew Treinish6900ba12013-02-19 16:38:01 -0500141 self._log_response(resp, None)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500142 else:
Matthew Treinish6900ba12013-02-19 16:38:01 -0500143 self._log_response(resp, body_iter)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500144
145 return resp, body_iter
146
Matthew Treinish6900ba12013-02-19 16:38:01 -0500147 def _log_request(self, method, url, headers):
148 LOG.info('Request: ' + method + ' ' + url)
149 if headers:
150 headers_out = headers
151 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
152 token = headers['X-Auth-Token']
153 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
154 headers_out = headers.copy()
155 headers_out['X-Auth-Token'] = "<Token omitted>"
156 LOG.info('Request Headers: ' + str(headers_out))
157
158 def _log_response(self, resp, body):
159 status = str(resp.status)
160 LOG.info("Response Status: " + status)
161 if resp.getheaders():
162 LOG.info('Response Headers: ' + str(resp.getheaders()))
163 if body:
164 str_body = str(body)
165 length = len(body)
166 LOG.info('Response Body: ' + str_body[:2048])
167 if length >= 2048:
168 self.LOG.debug("Large body (%d) md5 summary: %s", length,
169 hashlib.md5(str_body).hexdigest())
170
Matthew Treinish72ea4422013-02-07 14:42:49 -0500171 def json_request(self, method, url, **kwargs):
172 kwargs.setdefault('headers', {})
173 kwargs['headers'].setdefault('Content-Type', 'application/json')
174
175 if 'body' in kwargs:
176 kwargs['body'] = json.dumps(kwargs['body'])
177
178 resp, body_iter = self._http_request(url, method, **kwargs)
179
180 if 'application/json' in resp.getheader('content-type', None):
181 body = ''.join([chunk for chunk in body_iter])
182 try:
183 body = json.loads(body)
184 except ValueError:
185 LOG.error('Could not decode response body as JSON')
186 else:
187 body = None
188
189 return resp, body
190
191 def raw_request(self, method, url, **kwargs):
192 kwargs.setdefault('headers', {})
193 kwargs['headers'].setdefault('Content-Type',
194 'application/octet-stream')
195 if 'body' in kwargs:
196 if (hasattr(kwargs['body'], 'read')
197 and method.lower() in ('post', 'put')):
198 # We use 'Transfer-Encoding: chunked' because
199 # body size may not always be known in advance.
200 kwargs['headers']['Transfer-Encoding'] = 'chunked'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000201
202 # Decorate the request with auth
203 req_url, kwargs['headers'], kwargs['body'] = \
204 self.auth_provider.auth_request(
205 method=method, url=url, headers=kwargs['headers'],
206 body=kwargs.get('body', None), filters=self.filters)
207 return self._http_request(req_url, method, **kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500208
209
210class OpenSSLConnectionDelegator(object):
211 """
212 An OpenSSL.SSL.Connection delegator.
213
214 Supplies an additional 'makefile' method which httplib requires
215 and is not present in OpenSSL.SSL.Connection.
216
217 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
218 a delegator must be used.
219 """
220 def __init__(self, *args, **kwargs):
221 self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
222
223 def __getattr__(self, name):
224 return getattr(self.connection, name)
225
226 def makefile(self, *args, **kwargs):
Matthew Treinishab23e902014-01-27 22:18:15 +0000227 # Ensure the socket is closed when this file is closed
228 kwargs['close'] = True
Matthew Treinish72ea4422013-02-07 14:42:49 -0500229 return socket._fileobject(self.connection, *args, **kwargs)
230
231
232class VerifiedHTTPSConnection(httplib.HTTPSConnection):
233 """
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.
238 """
239 def __init__(self, host, port=None, key_file=None, cert_file=None,
240 cacert=None, timeout=None, insecure=False,
241 ssl_compression=True):
242 httplib.HTTPSConnection.__init__(self, host, port,
243 key_file=key_file,
244 cert_file=cert_file)
245 self.key_file = key_file
246 self.cert_file = cert_file
247 self.timeout = timeout
248 self.insecure = insecure
249 self.ssl_compression = ssl_compression
250 self.cacert = cacert
251 self.setcontext()
252
253 @staticmethod
254 def host_matches_cert(host, x509):
255 """
256 Verify that the the x509 certificate we have received
257 from 'host' correctly identifies the server we are
258 connecting to, ie that the certificate's Common Name
259 or a Subject Alternative Name matches 'host'.
260 """
261 # First see if we can match the CN
262 if x509.get_subject().commonName == host:
263 return True
264
265 # Also try Subject Alternative Names for a match
266 san_list = None
267 for i in xrange(x509.get_extension_count()):
268 ext = x509.get_extension(i)
269 if ext.get_short_name() == 'subjectAltName':
270 san_list = str(ext)
271 for san in ''.join(san_list.split()).split(','):
272 if san == "DNS:%s" % host:
273 return True
274
275 # Server certificate does not match host
276 msg = ('Host "%s" does not match x509 certificate contents: '
277 'CommonName "%s"' % (host, x509.get_subject().commonName))
278 if san_list is not None:
279 msg = msg + ', subjectAltName "%s"' % san_list
280 raise exc.SSLCertificateError(msg)
281
282 def verify_callback(self, connection, x509, errnum,
283 depth, preverify_ok):
284 if x509.has_expired():
285 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
286 raise exc.SSLCertificateError(msg)
287
288 if depth == 0 and preverify_ok is True:
289 # We verify that the host matches against the last
290 # certificate in the chain
291 return self.host_matches_cert(self.host, x509)
292 else:
293 # Pass through OpenSSL's default result
294 return preverify_ok
295
296 def setcontext(self):
297 """
298 Set up the OpenSSL context.
299 """
300 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
301
302 if self.ssl_compression is False:
303 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
304
305 if self.insecure is not True:
306 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
307 self.verify_callback)
308 else:
309 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
310 self.verify_callback)
311
312 if self.cert_file:
313 try:
314 self.context.use_certificate_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200315 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500316 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
317 raise exc.SSLConfigurationError(msg)
318 if self.key_file is None:
319 # We support having key and cert in same file
320 try:
321 self.context.use_privatekey_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200322 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500323 msg = ('No key file specified and unable to load key '
324 'from "%s" %s' % (self.cert_file, e))
325 raise exc.SSLConfigurationError(msg)
326
327 if self.key_file:
328 try:
329 self.context.use_privatekey_file(self.key_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200330 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500331 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
332 raise exc.SSLConfigurationError(msg)
333
334 if self.cacert:
335 try:
336 self.context.load_verify_locations(self.cacert)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200337 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500338 msg = 'Unable to load CA from "%s"' % (self.cacert, e)
339 raise exc.SSLConfigurationError(msg)
340 else:
341 self.context.set_default_verify_paths()
342
343 def connect(self):
344 """
345 Connect to an SSL port using the OpenSSL library and apply
346 per-connection parameters.
347 """
348 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
349 if self.timeout is not None:
350 # '0' microseconds
351 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
352 struct.pack('LL', self.timeout, 0))
353 self.sock = OpenSSLConnectionDelegator(self.context, sock)
354 self.sock.connect((self.host, self.port))
355
Matthew Treinishab23e902014-01-27 22:18:15 +0000356 def close(self):
357 if self.sock:
358 # Remove the reference to the socket but don't close it yet.
359 # Response close will close both socket and associated
360 # file. Closing socket too soon will cause response
361 # reads to fail with socket IO error 'Bad file descriptor'.
362 self.sock = None
Matthew Treinish7741cd62014-01-28 16:10:40 +0000363 httplib.HTTPSConnection.close(self)
Matthew Treinishab23e902014-01-27 22:18:15 +0000364
Matthew Treinish72ea4422013-02-07 14:42:49 -0500365
366class ResponseBodyIterator(object):
367 """A class that acts as an iterator over an HTTP response."""
368
369 def __init__(self, resp):
370 self.resp = resp
371
372 def __iter__(self):
373 while True:
374 yield self.next()
375
376 def next(self):
377 chunk = self.resp.read(CHUNKSIZE)
378 if chunk:
379 return chunk
380 else:
381 raise StopIteration()