blob: 935885149d0445e4190551d17d010541c75b61a4 [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
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -050022import OpenSSL
Matthew Treinish72ea4422013-02-07 14:42:49 -050023import posixpath
Matthew Treinish6900ba12013-02-19 16:38:01 -050024import re
llg821243b20502014-02-22 10:32:49 +080025from six import moves
Matthew Treinish72ea4422013-02-07 14:42:49 -050026import socket
27import StringIO
28import struct
29import urlparse
30
Matthew Treinish72ea4422013-02-07 14:42:49 -050031from tempest import exceptions as exc
Matthew Treinishf4a9b0f2013-07-26 16:58:26 -040032from tempest.openstack.common import log as logging
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
52 self.connection_class = self.get_connection_class(self.endpoint_scheme)
53 self.connection_kwargs = self.get_connection_kwargs(
54 self.endpoint_scheme, **kwargs)
55
Matthew Treinish72ea4422013-02-07 14:42:49 -050056 @staticmethod
Matthew Treinish72ea4422013-02-07 14:42:49 -050057 def get_connection_class(scheme):
58 if scheme == 'https':
59 return VerifiedHTTPSConnection
60 else:
61 return httplib.HTTPConnection
62
63 @staticmethod
64 def get_connection_kwargs(scheme, **kwargs):
65 _kwargs = {'timeout': float(kwargs.get('timeout', 600))}
66
67 if scheme == 'https':
68 _kwargs['cacert'] = kwargs.get('cacert', None)
69 _kwargs['cert_file'] = kwargs.get('cert_file', None)
70 _kwargs['key_file'] = kwargs.get('key_file', None)
71 _kwargs['insecure'] = kwargs.get('insecure', False)
72 _kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
73
74 return _kwargs
75
76 def get_connection(self):
77 _class = self.connection_class
78 try:
79 return _class(self.endpoint_hostname, self.endpoint_port,
80 **self.connection_kwargs)
81 except httplib.InvalidURL:
82 raise exc.EndpointNotFound
83
Matthew Treinish72ea4422013-02-07 14:42:49 -050084 def _http_request(self, url, method, **kwargs):
Attila Fazekasb2902af2013-02-16 16:22:44 +010085 """Send an http request with the specified characteristics.
Matthew Treinish72ea4422013-02-07 14:42:49 -050086
87 Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
88 as setting headers and error handling.
89 """
90 # Copy the kwargs so we can reuse the original in case of redirects
91 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
92 kwargs['headers'].setdefault('User-Agent', USER_AGENT)
Matthew Treinish72ea4422013-02-07 14:42:49 -050093
Matthew Treinish6900ba12013-02-19 16:38:01 -050094 self._log_request(method, url, kwargs['headers'])
95
Matthew Treinish72ea4422013-02-07 14:42:49 -050096 conn = self.get_connection()
97
98 try:
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -050099 url_parts = urlparse.urlparse(url)
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000100 conn_url = posixpath.normpath(url_parts.path)
101 LOG.debug('Actual Path: {path}'.format(path=conn_url))
Matthew Treinish72ea4422013-02-07 14:42:49 -0500102 if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
103 conn.putrequest(method, conn_url)
104 for header, value in kwargs['headers'].items():
105 conn.putheader(header, value)
106 conn.endheaders()
107 chunk = kwargs['body'].read(CHUNKSIZE)
108 # Chunk it, baby...
109 while chunk:
110 conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
111 chunk = kwargs['body'].read(CHUNKSIZE)
112 conn.send('0\r\n\r\n')
113 else:
114 conn.request(method, conn_url, **kwargs)
115 resp = conn.getresponse()
116 except socket.gaierror as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400117 message = ("Error finding address for %(url)s: %(e)s" %
118 {'url': url, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100119 raise exc.EndpointNotFound(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500120 except (socket.error, socket.timeout) as e:
Sean Dague43cd9052013-07-19 12:20:04 -0400121 message = ("Error communicating with %(endpoint)s %(e)s" %
122 {'endpoint': self.endpoint, 'e': e})
Attila Fazekasc7920282013-03-01 13:04:54 +0100123 raise exc.TimeoutException(message)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500124
125 body_iter = ResponseBodyIterator(resp)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500126 # Read body into string if it isn't obviously image data
127 if resp.getheader('content-type', None) != 'application/octet-stream':
Monty Taylorb2ca5ca2013-04-28 18:00:21 -0700128 body_str = ''.join([body_chunk for body_chunk in body_iter])
Matthew Treinish72ea4422013-02-07 14:42:49 -0500129 body_iter = StringIO.StringIO(body_str)
Matthew Treinish6900ba12013-02-19 16:38:01 -0500130 self._log_response(resp, None)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500131 else:
Matthew Treinish6900ba12013-02-19 16:38:01 -0500132 self._log_response(resp, body_iter)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500133
134 return resp, body_iter
135
Matthew Treinish6900ba12013-02-19 16:38:01 -0500136 def _log_request(self, method, url, headers):
137 LOG.info('Request: ' + method + ' ' + url)
138 if headers:
139 headers_out = headers
140 if 'X-Auth-Token' in headers and headers['X-Auth-Token']:
141 token = headers['X-Auth-Token']
142 if len(token) > 64 and TOKEN_CHARS_RE.match(token):
143 headers_out = headers.copy()
144 headers_out['X-Auth-Token'] = "<Token omitted>"
145 LOG.info('Request Headers: ' + str(headers_out))
146
147 def _log_response(self, resp, body):
148 status = str(resp.status)
149 LOG.info("Response Status: " + status)
150 if resp.getheaders():
151 LOG.info('Response Headers: ' + str(resp.getheaders()))
152 if body:
153 str_body = str(body)
154 length = len(body)
155 LOG.info('Response Body: ' + str_body[:2048])
156 if length >= 2048:
157 self.LOG.debug("Large body (%d) md5 summary: %s", length,
158 hashlib.md5(str_body).hexdigest())
159
Matthew Treinish72ea4422013-02-07 14:42:49 -0500160 def json_request(self, method, url, **kwargs):
161 kwargs.setdefault('headers', {})
162 kwargs['headers'].setdefault('Content-Type', 'application/json')
163
164 if 'body' in kwargs:
165 kwargs['body'] = json.dumps(kwargs['body'])
166
167 resp, body_iter = self._http_request(url, method, **kwargs)
168
Mauro S. M. Rodrigues568638f2014-03-05 09:09:38 -0500169 if 'application/json' in resp.getheader('content-type', ''):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500170 body = ''.join([chunk for chunk in body_iter])
171 try:
172 body = json.loads(body)
173 except ValueError:
174 LOG.error('Could not decode response body as JSON')
175 else:
176 body = None
177
178 return resp, body
179
180 def raw_request(self, method, url, **kwargs):
181 kwargs.setdefault('headers', {})
182 kwargs['headers'].setdefault('Content-Type',
183 'application/octet-stream')
184 if 'body' in kwargs:
185 if (hasattr(kwargs['body'], 'read')
186 and method.lower() in ('post', 'put')):
187 # We use 'Transfer-Encoding: chunked' because
188 # body size may not always be known in advance.
189 kwargs['headers']['Transfer-Encoding'] = 'chunked'
Andrea Frittoli8bbdb162014-01-06 11:06:13 +0000190
191 # Decorate the request with auth
192 req_url, kwargs['headers'], kwargs['body'] = \
193 self.auth_provider.auth_request(
194 method=method, url=url, headers=kwargs['headers'],
195 body=kwargs.get('body', None), filters=self.filters)
196 return self._http_request(req_url, method, **kwargs)
Matthew Treinish72ea4422013-02-07 14:42:49 -0500197
198
199class OpenSSLConnectionDelegator(object):
200 """
201 An OpenSSL.SSL.Connection delegator.
202
203 Supplies an additional 'makefile' method which httplib requires
204 and is not present in OpenSSL.SSL.Connection.
205
206 Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
207 a delegator must be used.
208 """
209 def __init__(self, *args, **kwargs):
210 self.connection = OpenSSL.SSL.Connection(*args, **kwargs)
211
212 def __getattr__(self, name):
213 return getattr(self.connection, name)
214
215 def makefile(self, *args, **kwargs):
Matthew Treinishab23e902014-01-27 22:18:15 +0000216 # Ensure the socket is closed when this file is closed
217 kwargs['close'] = True
Matthew Treinish72ea4422013-02-07 14:42:49 -0500218 return socket._fileobject(self.connection, *args, **kwargs)
219
220
221class VerifiedHTTPSConnection(httplib.HTTPSConnection):
222 """
223 Extended HTTPSConnection which uses the OpenSSL library
224 for enhanced SSL support.
225 Note: Much of this functionality can eventually be replaced
226 with native Python 3.3 code.
227 """
228 def __init__(self, host, port=None, key_file=None, cert_file=None,
229 cacert=None, timeout=None, insecure=False,
230 ssl_compression=True):
231 httplib.HTTPSConnection.__init__(self, host, port,
232 key_file=key_file,
233 cert_file=cert_file)
234 self.key_file = key_file
235 self.cert_file = cert_file
236 self.timeout = timeout
237 self.insecure = insecure
238 self.ssl_compression = ssl_compression
239 self.cacert = cacert
240 self.setcontext()
241
242 @staticmethod
243 def host_matches_cert(host, x509):
244 """
245 Verify that the the x509 certificate we have received
246 from 'host' correctly identifies the server we are
247 connecting to, ie that the certificate's Common Name
248 or a Subject Alternative Name matches 'host'.
249 """
250 # First see if we can match the CN
251 if x509.get_subject().commonName == host:
252 return True
253
254 # Also try Subject Alternative Names for a match
255 san_list = None
llg821243b20502014-02-22 10:32:49 +0800256 for i in moves.xrange(x509.get_extension_count()):
Matthew Treinish72ea4422013-02-07 14:42:49 -0500257 ext = x509.get_extension(i)
258 if ext.get_short_name() == 'subjectAltName':
259 san_list = str(ext)
260 for san in ''.join(san_list.split()).split(','):
261 if san == "DNS:%s" % host:
262 return True
263
264 # Server certificate does not match host
265 msg = ('Host "%s" does not match x509 certificate contents: '
266 'CommonName "%s"' % (host, x509.get_subject().commonName))
267 if san_list is not None:
268 msg = msg + ', subjectAltName "%s"' % san_list
269 raise exc.SSLCertificateError(msg)
270
271 def verify_callback(self, connection, x509, errnum,
272 depth, preverify_ok):
273 if x509.has_expired():
274 msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
275 raise exc.SSLCertificateError(msg)
276
277 if depth == 0 and preverify_ok is True:
278 # We verify that the host matches against the last
279 # certificate in the chain
280 return self.host_matches_cert(self.host, x509)
281 else:
282 # Pass through OpenSSL's default result
283 return preverify_ok
284
285 def setcontext(self):
286 """
287 Set up the OpenSSL context.
288 """
289 self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
290
291 if self.ssl_compression is False:
292 self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
293
294 if self.insecure is not True:
295 self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
296 self.verify_callback)
297 else:
298 self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
299 self.verify_callback)
300
301 if self.cert_file:
302 try:
303 self.context.use_certificate_file(self.cert_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200304 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500305 msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
306 raise exc.SSLConfigurationError(msg)
307 if self.key_file is None:
308 # We support having key and cert in same file
309 try:
310 self.context.use_privatekey_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 = ('No key file specified and unable to load key '
313 'from "%s" %s' % (self.cert_file, e))
314 raise exc.SSLConfigurationError(msg)
315
316 if self.key_file:
317 try:
318 self.context.use_privatekey_file(self.key_file)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200319 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500320 msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
321 raise exc.SSLConfigurationError(msg)
322
323 if self.cacert:
324 try:
325 self.context.load_verify_locations(self.cacert)
Dirk Mueller1db5db22013-06-23 20:21:32 +0200326 except Exception as e:
Matthew Treinish72ea4422013-02-07 14:42:49 -0500327 msg = 'Unable to load CA from "%s"' % (self.cacert, e)
328 raise exc.SSLConfigurationError(msg)
329 else:
330 self.context.set_default_verify_paths()
331
332 def connect(self):
333 """
334 Connect to an SSL port using the OpenSSL library and apply
335 per-connection parameters.
336 """
337 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
338 if self.timeout is not None:
339 # '0' microseconds
340 sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
341 struct.pack('LL', self.timeout, 0))
342 self.sock = OpenSSLConnectionDelegator(self.context, sock)
343 self.sock.connect((self.host, self.port))
344
Matthew Treinishab23e902014-01-27 22:18:15 +0000345 def close(self):
346 if self.sock:
347 # Remove the reference to the socket but don't close it yet.
348 # Response close will close both socket and associated
349 # file. Closing socket too soon will cause response
350 # reads to fail with socket IO error 'Bad file descriptor'.
351 self.sock = None
Matthew Treinish7741cd62014-01-28 16:10:40 +0000352 httplib.HTTPSConnection.close(self)
Matthew Treinishab23e902014-01-27 22:18:15 +0000353
Matthew Treinish72ea4422013-02-07 14:42:49 -0500354
355class ResponseBodyIterator(object):
356 """A class that acts as an iterator over an HTTP response."""
357
358 def __init__(self, resp):
359 self.resp = resp
360
361 def __iter__(self):
362 while True:
363 yield self.next()
364
365 def next(self):
366 chunk = self.resp.read(CHUNKSIZE)
367 if chunk:
368 return chunk
369 else:
370 raise StopIteration()