Add new common/http

Change-Id: I35ac71af1f648537231b01cca9be455bddd3cc80
This commit is contained in:
Darja Malyavkina 2016-09-23 14:39:20 +03:00
parent e76f8b02bc
commit b28abd04d2
10 changed files with 855 additions and 1337 deletions

View File

@ -13,48 +13,24 @@
# License for the specific language governing permissions and limitations
# under the License.
import warnings
from glareclient.common import utils
def Client(version=None, endpoint=None, session=None, *args, **kwargs):
def Client(version='1', endpoint=None, session=None, *args, **kwargs):
"""Client for the Glare Artifact Repository.
Generic client for the Glare Artifact Repository. See version classes
for specific details.
:param string version: The version of API to use.
:param session: A keystoneauth1 session that should be used for transport.
:type session: keystoneauth1.session.Session
"""
# FIXME(jamielennox): Add a deprecation warning if no session is passed.
# Leaving it as an option until we can ensure nothing break when we switch.
if session:
if endpoint:
kwargs.setdefault('endpoint_override', endpoint)
if not version:
__, version = utils.strip_version(endpoint)
if endpoint is not None:
kwargs.setdefault('endpoint_override', endpoint)
if not version:
msg = ("You must provide a client version when using session")
raise RuntimeError(msg)
else:
if version is not None:
warnings.warn(("`version` keyword is being deprecated. Please pass"
" the version as part of the URL. "
"http://$HOST:$PORT/v$VERSION_NUMBER"),
DeprecationWarning)
endpoint, url_version = utils.strip_version(endpoint)
version = version or url_version
if not version:
msg = ("Please provide either the version or an url with the form "
"http://$HOST:$PORT/v$VERSION_NUMBER")
raise RuntimeError(msg)
if version is None:
raise RuntimeError("You must provide a client version")
module = utils.import_versioned_module(int(version), 'client')
client_class = getattr(module, 'Client')

View File

@ -1,4 +1,4 @@
# Copyright 2012 OpenStack Foundation
# Copyright 2012 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -14,102 +14,79 @@
# under the License.
import copy
import logging
import hashlib
import os
import socket
from keystoneauth1 import adapter
from keystoneauth1 import exceptions as ksa_exc
from oslo_utils import importutils
from oslo_utils import netutils
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import requests
import six
import warnings
from six.moves import urllib
try:
import json
except ImportError:
import simplejson as json
from oslo_utils import encodeutils
from glareclient.common import utils
from glareclient import exc
osprofiler_web = importutils.try_import("osprofiler.web")
from glareclient._i18n import _
from glareclient.common import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-glareclient'
CHUNKSIZE = 1024 * 64 # 64kB
def encode_headers(headers):
"""Encodes headers.
Note: This should be used right before
sending anything out.
:param headers: Headers to encode
:returns: Dictionary with encoded headers'
names and values
"""
return dict((encodeutils.safe_encode(h), encodeutils.safe_encode(v))
for h, v in six.iteritems(headers) if v is not None)
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem',
'/System/Library/OpenSSL/certs/cacert.pem',
requests.certs.where()]
for ca in ca_path:
LOG.debug("Looking for ca file %s", ca)
if os.path.exists(ca):
LOG.debug("Using ca file %s", ca)
return ca
LOG.warning("System ca file could not be found.")
class _BaseHTTPClient(object):
def _chunk_body(body):
chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
if not chunk:
break
yield chunk
@staticmethod
def _chunk_body(body):
chunk = body
while chunk:
chunk = body.read(CHUNKSIZE)
if not chunk:
break
yield chunk
def _set_common_request_kwargs(self, headers, kwargs):
"""Handle the common parameters used to send the request."""
def _set_request_params(kwargs_params):
data = kwargs_params.pop('data', None)
params = copy.deepcopy(kwargs_params)
headers = params.get('headers', {})
content_type = headers.get('Content-Type', 'application/json')
# Default Content-Type is octet-stream
content_type = headers.get('Content-Type', 'application/json')
data = kwargs.pop("data", None)
if data is not None and not isinstance(data, six.string_types):
try:
data = json.dumps(data)
except TypeError:
data = self._chunk_body(data)
content_type = 'application/octet-stream'
if data is not None and not isinstance(data, six.string_types):
if content_type.startswith('application/json'):
data = jsonutils.dumps(data)
if content_type == 'application/octet-stream':
data = _chunk_body(data)
headers['Content-Type'] = content_type
kwargs['stream'] = content_type == 'application/octet-stream'
return data
params['data'] = data
headers.update({'Content-Type': content_type})
params['headers'] = headers
params['stream'] = content_type == 'application/octet-stream'
def _handle_response(self, resp):
# log request-id for each api cal
request_id = resp.headers.get('x-openstack-request-id')
if request_id:
LOG.debug('%(method)s call to glare-api for '
'%(url)s used request id '
'%(response_request_id)s',
{'method': resp.request.method,
'url': resp.url,
'response_request_id': request_id})
return params
if not resp.ok:
LOG.debug("Request returned failure status %s.", resp.status_code)
raise exc.from_response(resp, resp.content)
elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and
resp.request.path_url != '/versions'):
# NOTE(flaper87): Eventually, we'll remove the check on `versions`
# which is a bug (1491350) on the server.
raise exc.from_response(resp)
def _handle_response(resp):
content_type = resp.headers.get('Content-Type')
if not content_type:
body_iter = six.StringIO(resp.text)
try:
body_iter = json.loads(''.join([c for c in body_iter]))
body_iter = jsonutils.loads(''.join([c for c in body_iter]))
except ValueError:
body_iter = None
elif content_type.startswith('application/json'):
@ -122,172 +99,6 @@ class _BaseHTTPClient(object):
return resp, body_iter
class HTTPClient(_BaseHTTPClient):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.identity_headers = kwargs.get('identity_headers')
self.auth_token = kwargs.get('token')
self.language_header = kwargs.get('language_header')
self.last_request_id = None
if self.identity_headers:
if self.identity_headers.get('X-Auth-Token'):
self.auth_token = self.identity_headers.get('X-Auth-Token')
del self.identity_headers['X-Auth-Token']
self.session = requests.Session()
self.session.headers["User-Agent"] = USER_AGENT
if self.language_header:
self.session.headers["Accept-Language"] = self.language_header
self.timeout = float(kwargs.get('timeout', 600))
if self.endpoint.startswith("https"):
compression = kwargs.get('ssl_compression', True)
if compression is False:
# Note: This is not seen by default. (python must be
# run with -Wd)
warnings.warn('The "ssl_compression" argument has been '
'deprecated.', DeprecationWarning)
if kwargs.get('insecure', False) is True:
self.session.verify = False
else:
if kwargs.get('cacert', None) is not '':
self.session.verify = kwargs.get('cacert', True)
self.session.cert = (kwargs.get('cert_file'),
kwargs.get('key_file'))
@staticmethod
def parse_endpoint(endpoint):
return netutils.urlsplit(endpoint)
def log_curl_request(self, method, url, headers, data, kwargs):
curl = ['curl -g -i -X %s' % method]
headers = copy.deepcopy(headers)
headers.update(self.session.headers)
for (key, value) in six.iteritems(headers):
header = '-H \'%s: %s\'' % utils.safe_header(key, value)
curl.append(header)
if not self.session.verify:
curl.append('-k')
else:
if isinstance(self.session.verify, six.string_types):
curl.append(' --cacert %s' % self.session.verify)
if self.session.cert:
curl.append(' --cert %s --key %s' % self.session.cert)
if data and isinstance(data, six.string_types):
curl.append('-d \'%s\'' % data)
curl.append(url)
msg = ' '.join([encodeutils.safe_decode(item, errors='ignore')
for item in curl])
LOG.debug(msg)
@staticmethod
def log_http_response(resp):
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
headers = resp.headers.items()
dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers])
dump.append('')
content_type = resp.headers.get('Content-Type')
if content_type != 'application/octet-stream':
dump.extend([resp.text, ''])
LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore')
for x in dump]))
def _request(self, method, url, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
headers = copy.deepcopy(kwargs.pop('headers', {}))
if self.identity_headers:
for k, v in six.iteritems(self.identity_headers):
headers.setdefault(k, v)
data = self._set_common_request_kwargs(headers, kwargs)
# add identity header to the request
if not headers.get('X-Auth-Token'):
headers['X-Auth-Token'] = self.auth_token
if osprofiler_web:
headers.update(osprofiler_web.get_trace_id_headers())
# Note(flaper87): Before letting headers / url fly,
# they should be encoded otherwise httplib will
# complain.
headers = encode_headers(headers)
if self.endpoint.endswith("/") or url.startswith("/"):
conn_url = "%s%s" % (self.endpoint, url)
else:
conn_url = "%s/%s" % (self.endpoint, url)
self.log_curl_request(method, conn_url, headers, data, kwargs)
try:
resp = self.session.request(method,
conn_url,
data=data,
headers=headers,
**kwargs)
except requests.exceptions.Timeout as e:
message = ("Error communicating with %(url)s: %(e)s" %
dict(url=conn_url, e=e))
raise exc.InvalidEndpoint(message=message)
except requests.exceptions.ConnectionError as e:
message = ("Error finding address for %(url)s: %(e)s" %
dict(url=conn_url, e=e))
raise exc.CommunicationError(message=message)
except socket.gaierror as e:
message = "Error finding address for %s: %s" % (
self.endpoint_hostname, e)
raise exc.InvalidEndpoint(message=message)
except (socket.error, socket.timeout) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
self.last_request_id = resp.headers.get('x-openstack-request-id')
resp, body_iter = self._handle_response(resp)
self.log_http_response(resp)
return resp, body_iter
def head(self, url, **kwargs):
return self._request('HEAD', url, **kwargs)
def get(self, url, **kwargs):
return self._request('GET', url, **kwargs)
def post(self, url, **kwargs):
return self._request('POST', url, **kwargs)
def put(self, url, **kwargs):
return self._request('PUT', url, **kwargs)
def patch(self, url, **kwargs):
return self._request('PATCH', url, **kwargs)
def delete(self, url, **kwargs):
return self._request('DELETE', url, **kwargs)
def _close_after_stream(response, chunk_size):
"""Iterate over the content and ensure the response is closed after."""
# Yield each chunk in the response body
@ -299,45 +110,278 @@ def _close_after_stream(response, chunk_size):
response.close()
class SessionClient(adapter.Adapter, _BaseHTTPClient):
class HTTPClient(object):
def __init__(self, session, **kwargs):
kwargs.setdefault('user_agent', USER_AGENT)
kwargs.setdefault('service_type', 'artifact')
self.last_request_id = None
super(SessionClient, self).__init__(session, **kwargs)
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.auth_url = kwargs.get('auth_url')
self.auth_token = kwargs.get('token')
self.username = kwargs.get('username')
self.password = kwargs.get('password')
self.region_name = kwargs.get('region_name')
self.include_pass = kwargs.get('include_pass')
self.endpoint_url = endpoint
def request(self, url, method, **kwargs):
headers = encode_headers(kwargs.pop('headers', {}))
kwargs['raise_exc'] = False
data = self._set_common_request_kwargs(headers, kwargs)
self.cert_file = kwargs.get('cert_file')
self.key_file = kwargs.get('key_file')
self.timeout = kwargs.get('timeout')
self.ssl_connection_params = {
'cacert': kwargs.get('cacert'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'insecure': kwargs.get('insecure'),
}
self.verify_cert = None
if urllib.parse.urlparse(endpoint).scheme == "https":
if kwargs.get('insecure'):
self.verify_cert = False
else:
self.verify_cert = kwargs.get('cacert', get_system_ca_file())
def _safe_header(self, name, value):
if name in ['X-Auth-Token', 'X-Subject-Token']:
# because in python3 byte string handling is ... ug
v = value.encode('utf-8')
h = hashlib.sha1(v)
d = h.hexdigest()
return encodeutils.safe_decode(name), "{SHA1}%s" % d
else:
return (encodeutils.safe_decode(name),
encodeutils.safe_decode(value))
def log_curl_request(self, url, method, kwargs):
curl = ['curl -i -X %s' % method]
for (key, value) in kwargs['headers'].items():
header = '-H \'%s: %s\'' % self._safe_header(key, value)
curl.append(header)
conn_params_fmt = [
('key_file', '--key %s'),
('cert_file', '--cert %s'),
('cacert', '--cacert %s'),
]
for (key, fmt) in conn_params_fmt:
value = self.ssl_connection_params.get(key)
if value:
curl.append(fmt % value)
if self.ssl_connection_params.get('insecure'):
curl.append('-k')
if 'data' in kwargs:
curl.append('-d \'%s\'' % kwargs['data'])
curl.append('%s%s' % (self.endpoint, url))
LOG.debug(' '.join(curl))
@staticmethod
def log_http_response(resp):
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
dump = ['\nHTTP/%.1f %s %s' % status]
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
dump.append('')
if resp.content:
content = resp.content
if isinstance(content, six.binary_type):
try:
content = encodeutils.safe_decode(resp.content)
except UnicodeDecodeError:
pass
else:
dump.extend([content, ''])
LOG.debug('\n'.join(dump))
def request(self, url, method, log=True, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around requests.request to handle tasks such
as setting headers and error handling.
"""
# Copy the kwargs so we can reuse the original in case of redirects
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
if self.auth_token:
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
else:
kwargs['headers'].update(self.credentials_headers())
if self.auth_url:
kwargs['headers'].setdefault('X-Auth-Url', self.auth_url)
if self.region_name:
kwargs['headers'].setdefault('X-Region-Name', self.region_name)
self.log_curl_request(url, method, kwargs)
if self.cert_file and self.key_file:
kwargs['cert'] = (self.cert_file, self.key_file)
if self.verify_cert is not None:
kwargs['verify'] = self.verify_cert
if self.timeout is not None:
kwargs['timeout'] = float(self.timeout)
# Allow the option not to follow redirects
follow_redirects = kwargs.pop('follow_redirects', True)
# Since requests does not follow the RFC when doing redirection to sent
# back the same method on a redirect we are simply bypassing it. For
# example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says
# that we should follow that URL with the same method as before,
# requests doesn't follow that and send a GET instead for the method.
# Hopefully this could be fixed as they say in a comment in a future
# point version i.e.: 3.x
# See issue: https://github.com/kennethreitz/requests/issues/1704
allow_redirects = False
try:
resp = super(SessionClient, self).request(url,
method,
headers=headers,
data=data,
**kwargs)
except ksa_exc.ConnectTimeout as e:
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
message = ("Error communicating with %(url)s %(e)s" %
dict(url=conn_url, e=e))
raise exc.InvalidEndpoint(message=message)
except ksa_exc.ConnectFailure as e:
conn_url = self.get_endpoint(auth=kwargs.get('auth'))
conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/'))
resp = requests.request(
method,
self.endpoint_url + url,
allow_redirects=allow_redirects,
**kwargs)
except socket.gaierror as e:
message = ("Error finding address for %(url)s: %(e)s" %
dict(url=conn_url, e=e))
{'url': self.endpoint_url + url, 'e': e})
raise exc.InvalidEndpoint(message=message)
except (socket.error,
socket.timeout,
requests.exceptions.ConnectionError) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
self.last_request_id = resp.headers.get('x-openstack-request-id')
return self._handle_response(resp)
if log:
self.log_http_response(resp)
if 'X-Auth-Key' not in kwargs['headers'] and \
(resp.status_code == 401 or
(resp.status_code == 500 and
"(HTTP 401)" in resp.content)):
raise exc.HTTPUnauthorized("Authentication failed. Please try"
" again.\n%s"
% resp.content)
elif 400 <= resp.status_code < 600:
raise exc.from_response(resp)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location,
# unless caller specified follow_redirects=False
if follow_redirects:
location = resp.headers.get('location')
path = self.strip_endpoint(location)
resp = self.request(path, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp)
return resp
def strip_endpoint(self, location):
if location is None:
message = "Location not returned with 302"
raise exc.InvalidEndpoint(message=message)
elif location.startswith(self.endpoint):
return location[len(self.endpoint):]
else:
message = "Prohibited endpoint redirect %s" % location
raise exc.InvalidEndpoint(message=message)
def credentials_headers(self):
creds = {}
if self.username:
creds['X-Auth-User'] = self.username
if self.password:
creds['X-Auth-Key'] = self.password
return creds
def json_request(self, url, method, **kwargs):
params = _set_request_params(kwargs)
resp = self.request(url, method, **params)
return _handle_response(resp)
def json_patch_request(self, url, method='PATCH', **kwargs):
return self.json_request(
url, method, **kwargs)
def head(self, url, **kwargs):
return self.json_request(url, "HEAD", **kwargs)
def get(self, url, **kwargs):
return self.json_request(url, "GET", **kwargs)
def post(self, url, **kwargs):
return self.json_request(url, "POST", **kwargs)
def put(self, url, **kwargs):
return self.json_request(url, "PUT", **kwargs)
def delete(self, url, **kwargs):
return self.request(url, "DELETE", **kwargs)
def patch(self, url, **kwargs):
return self.json_request(url, "PATCH", **kwargs)
def get_http_client(endpoint=None, session=None, **kwargs):
class SessionClient(adapter.LegacyJsonAdapter):
"""HTTP client based on Keystone client session."""
def request(self, url, method, **kwargs):
params = _set_request_params(kwargs)
redirect = kwargs.get('redirect')
resp, body = super(SessionClient, self).request(
url, method,
**params)
if 400 <= resp.status_code < 600:
raise exc.from_response(resp)
elif resp.status_code in (301, 302, 305):
if redirect:
location = resp.headers.get('location')
path = self.strip_endpoint(location)
resp = self.request(path, method, **kwargs)
elif resp.status_code == 300:
raise exc.from_response(resp)
if resp.headers.get('Content-Type') == 'application/octet-stream':
body = _close_after_stream(resp, CHUNKSIZE)
return resp, body
def strip_endpoint(self, location):
if location is None:
message = _("Location not returned with 302")
raise exc.InvalidEndpoint(message=message)
if (self.endpoint_override is not None and
location.lower().startswith(self.endpoint_override.lower())):
return location[len(self.endpoint_override):]
else:
return location
def construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
endpoint = next(iter(args), None)
if session:
return SessionClient(session, **kwargs)
service_type = kwargs.pop('service_type', None)
endpoint_type = kwargs.pop('endpoint_type', None)
region_name = kwargs.pop('region_name', None)
service_name = kwargs.pop('service_name', None)
parameters = {
'endpoint_override': endpoint,
'session': session,
'auth': auth,
'interface': endpoint_type,
'service_type': service_type,
'region_name': region_name,
'service_name': service_name,
'user_agent': 'python-glareclient',
}
parameters.update(kwargs)
return SessionClient(**parameters)
elif endpoint:
return HTTPClient(endpoint, **kwargs)
else:

View File

@ -1,347 +0,0 @@
# Copyright 2014 Red Hat, Inc
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import socket
import ssl
import struct
import OpenSSL
from requests import adapters
from requests import compat
try:
from requests.packages.urllib3 import connectionpool
from requests.packages.urllib3 import poolmanager
except ImportError:
from urllib3 import connectionpool
from urllib3 import poolmanager
from oslo_utils import encodeutils
import six
# NOTE(jokke): simplified transition to py3, behaves like py2 xrange
from six.moves import range
try:
from eventlet import patcher
# Handle case where we are running in a monkey patched environment
if patcher.is_monkey_patched('socket'):
from eventlet.green.httplib import HTTPSConnection
from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
else:
raise ImportError
except ImportError:
from OpenSSL import SSL
from six.moves import http_client
HTTPSConnection = http_client.HTTPSConnection
Connection = SSL.Connection
from glareclient import exc
def verify_callback(host=None):
"""Provide wrapper for do_verify_callback.
We use a partial around the 'real' verify_callback function
so that we can stash the host value without holding a
reference on the VerifiedHTTPSConnection.
"""
def wrapper(connection, x509, errnum,
depth, preverify_ok, host=host):
return do_verify_callback(connection, x509, errnum,
depth, preverify_ok, host=host)
return wrapper
def do_verify_callback(connection, x509, errnum,
depth, preverify_ok, host=None):
"""Verify the server's SSL certificate.
This is a standalone function rather than a method to avoid
issues around closing sockets if a reference is held on
a VerifiedHTTPSConnection by the callback function.
"""
if x509.has_expired():
msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
raise exc.SSLCertificateError(msg)
if depth == 0 and preverify_ok:
# We verify that the host matches against the last
# certificate in the chain
return host_matches_cert(host, x509)
else:
# Pass through OpenSSL's default result
return preverify_ok
def host_matches_cert(host, x509):
"""Verify the certificate identifies the host.
Verify that the x509 certificate we have received
from 'host' correctly identifies the server we are
connecting to, ie that the certificate's Common Name
or a Subject Alternative Name matches 'host'.
"""
def check_match(name):
# Directly match the name
if name == host:
return True
# Support single wildcard matching
if name.startswith('*.') and host.find('.') > 0:
if name[2:] == host.split('.', 1)[1]:
return True
common_name = x509.get_subject().commonName
# First see if we can match the CN
if check_match(common_name):
return True
# Also try Subject Alternative Names for a match
san_list = None
for i in range(x509.get_extension_count()):
ext = x509.get_extension(i)
if ext.get_short_name() == b'subjectAltName':
san_list = str(ext)
for san in ''.join(san_list.split()).split(','):
if san.startswith('DNS:'):
if check_match(san.split(':', 1)[1]):
return True
# Server certificate does not match host
msg = ('Host "%s" does not match x509 certificate contents: '
'CommonName "%s"' % (host, common_name))
if san_list is not None:
msg = msg + ', subjectAltName "%s"' % san_list
raise exc.SSLCertificateError(msg)
def to_bytes(s):
if isinstance(s, six.string_types):
return six.b(s)
else:
return s
class HTTPSAdapter(adapters.HTTPAdapter):
"""This adapter will be used just when ssl compression should be disabled.
The init method overwrites the default https pool by setting
glareclient's one.
"""
def __init__(self, *args, **kwargs):
classes_by_scheme = poolmanager.pool_classes_by_scheme
classes_by_scheme["glare+https"] = HTTPSConnectionPool
super(HTTPSAdapter, self).__init__(*args, **kwargs)
def request_url(self, request, proxies):
# NOTE(flaper87): Make sure the url is encoded, otherwise
# python's standard httplib will fail with a TypeError.
url = super(HTTPSAdapter, self).request_url(request, proxies)
if six.PY2:
url = encodeutils.safe_encode(url)
return url
def _create_glare_httpsconnectionpool(self, url):
kw = self.poolmanager.connection_pool_kw
# Parse the url to get the scheme, host, and port
parsed = compat.urlparse(url)
# If there is no port specified, we should use the standard HTTPS port
port = parsed.port or 443
host = parsed.netloc.rsplit(':', 1)[0]
pool = HTTPSConnectionPool(host, port, **kw)
with self.poolmanager.pools.lock:
self.poolmanager.pools[(parsed.scheme, host, port)] = pool
return pool
def get_connection(self, url, proxies=None):
try:
return super(HTTPSAdapter, self).get_connection(url, proxies)
except KeyError:
# NOTE(sigamvirus24): This works around modifying a module global
# which fixes bug #1396550
# The scheme is most likely glare+https but check anyway
if not url.startswith('glare+https://'):
raise
return self._create_glare_httpsconnectionpool(url)
def cert_verify(self, conn, url, verify, cert):
super(HTTPSAdapter, self).cert_verify(conn, url, verify, cert)
conn.ca_certs = verify[0]
conn.insecure = verify[1]
class HTTPSConnectionPool(connectionpool.HTTPSConnectionPool):
"""A replacement for the default HTTPSConnectionPool.
HTTPSConnectionPool will be instantiated when a new
connection is requested to the HTTPSAdapter. This
implementation overwrites the _new_conn method and
returns an instances of glareclient's VerifiedHTTPSConnection
which handles no compression.
ssl_compression is hard-coded to False because this will
be used just when the user sets --no-ssl-compression.
"""
scheme = 'glare+https'
def _new_conn(self):
self.num_connections += 1
return VerifiedHTTPSConnection(host=self.host,
port=self.port,
key_file=self.key_file,
cert_file=self.cert_file,
cacert=self.ca_certs,
insecure=self.insecure,
ssl_compression=False)
class OpenSSLConnectionDelegator(object):
"""An OpenSSL.SSL.Connection delegator.
Supplies an additional 'makefile' method which httplib requires
and is not present in OpenSSL.SSL.Connection.
Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
a delegator must be used.
"""
def __init__(self, *args, **kwargs):
self.connection = Connection(*args, **kwargs)
def __getattr__(self, name):
return getattr(self.connection, name)
def makefile(self, *args, **kwargs):
return socket._fileobject(self.connection, *args, **kwargs)
class VerifiedHTTPSConnection(HTTPSConnection):
"""Extended OpenSSL HTTPSConnection for enhanced SSL support.
Note: Much of this functionality can eventually be replaced
with native Python 3.3 code.
"""
# Restrict the set of client supported cipher suites
CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\
'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\
'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS'
def __init__(self, host, port=None, key_file=None, cert_file=None,
cacert=None, timeout=None, insecure=False,
ssl_compression=True):
# List of exceptions reported by Python3 instead of
# SSLConfigurationError
if six.PY3:
excp_lst = (TypeError, FileNotFoundError, ssl.SSLError)
else:
# NOTE(jamespage)
# Accommodate changes in behaviour for pep-0467, introduced
# in python 2.7.9.
# https://github.com/python/peps/blob/master/pep-0476.txt
excp_lst = (TypeError, IOError, ssl.SSLError)
try:
HTTPSConnection.__init__(self, host, port,
key_file=key_file,
cert_file=cert_file)
self.key_file = key_file
self.cert_file = cert_file
self.timeout = timeout
self.insecure = insecure
# NOTE(flaper87): `is_verified` is needed for
# requests' urllib3. If insecure is True then
# the request is not `verified`, hence `not insecure`
self.is_verified = not insecure
self.ssl_compression = ssl_compression
self.cacert = None if cacert is None else str(cacert)
self.set_context()
# ssl exceptions are reported in various form in Python 3
# so to be compatible, we report the same kind as under
# Python2
except excp_lst as e:
raise exc.SSLConfigurationError(str(e))
def set_context(self):
"""Set up the OpenSSL context."""
self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
self.context.set_cipher_list(self.CIPHERS)
if self.ssl_compression is False:
self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
if self.insecure is not True:
self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
verify_callback(host=self.host))
else:
self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
lambda *args: True)
if self.cert_file:
try:
self.context.use_certificate_file(self.cert_file)
except Exception as e:
msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
raise exc.SSLConfigurationError(msg)
if self.key_file is None:
# We support having key and cert in same file
try:
self.context.use_privatekey_file(self.cert_file)
except Exception as e:
msg = ('No key file specified and unable to load key '
'from "%s" %s' % (self.cert_file, e))
raise exc.SSLConfigurationError(msg)
if self.key_file:
try:
self.context.use_privatekey_file(self.key_file)
except Exception as e:
msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
raise exc.SSLConfigurationError(msg)
if self.cacert:
try:
self.context.load_verify_locations(to_bytes(self.cacert))
except Exception as e:
msg = 'Unable to load CA from "%s" %s' % (self.cacert, e)
raise exc.SSLConfigurationError(msg)
else:
self.context.set_default_verify_paths()
def connect(self):
"""Connect to an SSL port using the OpenSSL library.
This method also applies per-connection parameters to the connection.
"""
result = socket.getaddrinfo(self.host, self.port, 0,
socket.SOCK_STREAM)
if result:
socket_family = result[0][0]
if socket_family == socket.AF_INET6:
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
else:
# If due to some reason the address lookup fails - we still connect
# to IPv4 socket. This retains the older behavior.
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
if self.timeout is not None:
# '0' microseconds
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
struct.pack('LL', self.timeout, 0))
self.sock = OpenSSLConnectionDelegator(self.context, sock)
self.sock.connect((self.host, self.port))

View File

@ -0,0 +1,48 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_serialization import jsonutils
class FakeRaw(object):
version = 110
class FakeHTTPResponse(object):
version = 1.1
def __init__(self, status_code, reason, headers, content):
self.headers = headers
self.content = content
self.text = content
self.status_code = status_code
self.reason = reason
self.raw = FakeRaw()
def getheader(self, name, default=None):
return self.headers.get(name, default)
def getheaders(self):
return self.headers.items()
def read(self, amt=None):
b = self.content
self.content = None
return b
def iter_content(self, chunksize):
return self.content
def json(self):
return jsonutils.loads(self.content)

View File

@ -1,55 +0,0 @@
# Copyright 2014 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from glareclient import client
from glareclient import v1
class ClientTest(testtools.TestCase):
def test_no_endpoint_error(self):
self.assertRaises(ValueError, client.Client, None)
def test_endpoint(self):
gc = client.Client('1', "http://example.com")
self.assertEqual("http://example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)
def test_versioned_endpoint(self):
gc = client.Client('1', "http://example.com/v1")
self.assertEqual("http://example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)
def test_versioned_endpoint_with_minor_revision(self):
gc = client.Client('1', "http://example.com/v1.0")
self.assertEqual("http://example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)
def test_endpoint_with_version_hostname(self):
gc = client.Client('1', "http://v1.example.com")
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)
def test_versioned_endpoint_with_version_hostname_v1(self):
gc = client.Client(endpoint="http://v2.example.com/v1")
self.assertEqual("http://v2.example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)
def test_versioned_endpoint_with_minor_revision_and_version_hostname(self):
gc = client.Client(endpoint="http://v1.example.com/v1.1")
self.assertEqual("http://v1.example.com", gc.http_client.endpoint)
self.assertIsInstance(gc, v1.client.Client)

View File

@ -0,0 +1,483 @@
# -*- coding:utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
import mock
import testtools
from glareclient.common import exceptions as exc
from glareclient.common import http
from glareclient.tests.unit import fakes
@mock.patch('glareclient.common.http.requests.request')
class HttpClientTest(testtools.TestCase):
# Patch os.environ to avoid required auth info.
def setUp(self):
super(HttpClientTest, self).setUp()
def test_http_raw_request(self, mock_request):
headers = {'User-Agent': 'python-glareclient'}
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
client = http.HTTPClient('http://example.com:9494')
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual('', ''.join([x for x in resp.content]))
mock_request.assert_called_with('GET', 'http://example.com:9494',
allow_redirects=False,
headers=headers)
def test_token_or_credentials(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.side_effect = [fake200, fake200, fake200]
# Replay, create client, assert
client = http.HTTPClient('http://example.com:9494')
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
client.username = 'user'
client.password = 'pass'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
client.auth_token = 'abcd1234'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
# no token or credentials
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:9494',
allow_redirects=False,
headers={'User-Agent': 'python-glareclient'}),
mock.call('GET', 'http://example.com:9494',
allow_redirects=False,
headers={'User-Agent': 'python-glareclient',
'X-Auth-Key': 'pass',
'X-Auth-User': 'user'}),
mock.call('GET', 'http://example.com:9494',
allow_redirects=False,
headers={'User-Agent': 'python-glareclient',
'X-Auth-Token': 'abcd1234'})
])
def test_region_name(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.return_value = fake200
client = http.HTTPClient('http://example.com:9494')
client.region_name = 'RegionOne'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
headers={'X-Region-Name': 'RegionOne',
'User-Agent': 'python-glareclient'})
def test_http_json_request(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request):
"""Check that we have sent the proper arguments to requests."""
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
client.verify_cert = True
client.cert_file = 'RANDOM_CERT_FILE'
client.key_file = 'RANDOM_KEY_FILE'
client.auth_url = 'http://AUTH_URL'
resp, body = client.json_request('', 'GET', data='text')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
verify=True,
data='text',
stream=False,
headers={'Content-Type': 'application/json',
'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-glareclient'})
def test_http_json_request_w_req_body(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET', data='test-body')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
data='test-body',
allow_redirects=False,
stream=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'not/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET', data='test-data')
self.assertEqual(200, resp.status_code)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494', data='test-data',
allow_redirects=False,
stream=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_invalid_json(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'invalid-json')
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertIsNone(body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_manual_redirect_delete(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:9494/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'DELETE')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('DELETE', 'http://example.com:9494/foo',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('DELETE', 'http://example.com:9494/foo/bar',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
])
def test_http_manual_redirect_post(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:9494/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'POST')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('POST', 'http://example.com:9494/foo',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('POST', 'http://example.com:9494/foo/bar',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
])
def test_http_manual_redirect_put(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:9494/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:9494/foo')
resp, body = client.json_request('', 'PUT')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('PUT', 'http://example.com:9494/foo',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('PUT', 'http://example.com:9494/foo/bar',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
])
def test_http_manual_redirect_prohibited(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:9494/'},
'')
client = http.HTTPClient('http://example.com:9494/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_manual_redirect_error_without_location(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{},
'')
client = http.HTTPClient('http://example.com:9494/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:9494/foo',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_json_request_redirect(self, mock_request):
# Record the 302
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:9494'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:9494')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'}),
mock.call('GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
])
def test_http_404_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
404, 'Not Found', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
e = self.assertRaises(exc.HTTPNotFound, client.json_request, '', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 404
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_http_300_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
300, 'OK', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494')
e = self.assertRaises(
exc.HTTPMultipleChoices, client.json_request, '', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 300
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'})
def test_fake_json_request(self, mock_request):
headers = {'User-Agent': 'python-glareclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('fake://example.com:9494')
self.assertRaises(exc.InvalidEndpoint,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'fake://example.com:9494/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_error(self, mock_request):
headers = {'User-Agent': 'python-glareclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('http://example.com:9494')
self.assertRaises(exc.InvalidEndpoint,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:9494/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_timeout(self, mock_request):
headers = {'User-Agent': 'python-glareclient'}
mock_request.side_effect = [socket.timeout]
client = http.HTTPClient('http://example.com:9494')
self.assertRaises(exc.CommunicationError,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:9494/',
allow_redirects=False,
headers=headers)
def test_http_request_specify_timeout(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:9494', timeout='123')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:9494',
allow_redirects=False,
stream=False,
data=None,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-glareclient'},
timeout=float(123))
def test_get_system_ca_file(self, mock_request):
chosen = '/etc/ssl/certs/ca-certificates.crt'
with mock.patch('os.path.exists') as mock_os:
mock_os.return_value = chosen
ca = http.get_system_ca_file()
self.assertEqual(chosen, ca)
mock_os.assert_called_once_with(chosen)
def test_insecure_verify_cert_None(self, mock_request):
client = http.HTTPClient('https://foo', insecure=True)
self.assertFalse(client.verify_cert)
def test_passed_cert_to_verify_cert(self, mock_request):
client = http.HTTPClient('https://foo', cacert="NOWHERE")
self.assertEqual("NOWHERE", client.verify_cert)
with mock.patch('glareclient.common.http.get_system_ca_file') as gsf:
gsf.return_value = "SOMEWHERE"
client = http.HTTPClient('https://foo')
self.assertEqual("SOMEWHERE", client.verify_cert)

View File

@ -1,401 +0,0 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import functools
import json
from keystoneauth1 import session
from keystoneauth1 import token_endpoint
import mock
import requests
from requests_mock.contrib import fixture
import six
from six.moves.urllib import parse
from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import testtools
from testtools import matchers
import types
import glareclient
from glareclient.common import http
from glareclient.tests import utils
def original_only(f):
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
if not hasattr(self.client, 'log_curl_request'):
self.skipTest('Skip logging tests for session client')
return f(self, *args, **kwargs)
class TestClient(testtools.TestCase):
scenarios = [
('httpclient', {'create_client': '_create_http_client'}),
('session', {'create_client': '_create_session_client'})
]
def _create_http_client(self):
return http.HTTPClient(self.endpoint, token=self.token)
def _create_session_client(self):
auth = token_endpoint.Token(self.endpoint, self.token)
sess = session.Session(auth=auth)
return http.SessionClient(sess)
def setUp(self):
super(TestClient, self).setUp()
self.mock = self.useFixture(fixture.Fixture())
self.endpoint = 'http://example.com:9292'
self.ssl_endpoint = 'https://example.com:9292'
self.token = u'abc123'
self.client = getattr(self, self.create_client)()
def test_identity_headers_and_token(self):
identity_headers = {
'X-Auth-Token': 'auth_token',
'X-User-Id': 'user',
'X-Tenant-Id': 'tenant',
'X-Roles': 'roles',
'X-Identity-Status': 'Confirmed',
'X-Service-Catalog': 'service_catalog',
}
# with token
kwargs = {'token': u'fake-token',
'identity_headers': identity_headers}
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
self.assertEqual('auth_token', http_client_object.auth_token)
self.assertTrue(http_client_object.identity_headers.
get('X-Auth-Token') is None)
def test_identity_headers_and_no_token_in_header(self):
identity_headers = {
'X-User-Id': 'user',
'X-Tenant-Id': 'tenant',
'X-Roles': 'roles',
'X-Identity-Status': 'Confirmed',
'X-Service-Catalog': 'service_catalog',
}
# without X-Auth-Token in identity headers
kwargs = {'token': u'fake-token',
'identity_headers': identity_headers}
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
self.assertEqual(u'fake-token', http_client_object.auth_token)
self.assertTrue(http_client_object.identity_headers.
get('X-Auth-Token') is None)
def test_identity_headers_and_no_token_in_session_header(self):
# Tests that if token or X-Auth-Token are not provided in the kwargs
# when creating the http client, the session headers don't contain
# the X-Auth-Token key.
identity_headers = {
'X-User-Id': 'user',
'X-Tenant-Id': 'tenant',
'X-Roles': 'roles',
'X-Identity-Status': 'Confirmed',
'X-Service-Catalog': 'service_catalog',
}
kwargs = {'identity_headers': identity_headers}
http_client_object = http.HTTPClient(self.endpoint, **kwargs)
self.assertIsNone(http_client_object.auth_token)
self.assertNotIn('X-Auth-Token', http_client_object.session.headers)
def test_identity_headers_are_passed(self):
# Tests that if token or X-Auth-Token are not provided in the kwargs
# when creating the http client, the session headers don't contain
# the X-Auth-Token key.
identity_headers = {
'X-User-Id': b'user',
'X-Tenant-Id': b'tenant',
'X-Roles': b'roles',
'X-Identity-Status': b'Confirmed',
'X-Service-Catalog': b'service_catalog',
}
kwargs = {'identity_headers': identity_headers}
http_client = http.HTTPClient(self.endpoint, **kwargs)
path = '/artifactsmy-image'
self.mock.get(self.endpoint + path)
http_client.get(path)
headers = self.mock.last_request.headers
for k, v in six.iteritems(identity_headers):
self.assertEqual(v, headers[k])
def test_language_header_passed(self):
kwargs = {'language_header': 'nb_NO'}
http_client = http.HTTPClient(self.endpoint, **kwargs)
path = '/v2/images/my-image'
self.mock.get(self.endpoint + path)
http_client.get(path)
headers = self.mock.last_request.headers
self.assertEqual(kwargs['language_header'], headers['Accept-Language'])
def test_language_header_not_passed_no_language(self):
kwargs = {}
http_client = http.HTTPClient(self.endpoint, **kwargs)
path = '/v2/images/my-image'
self.mock.get(self.endpoint + path)
http_client.get(path)
headers = self.mock.last_request.headers
self.assertTrue('Accept-Language' not in headers)
def test_connection_timeout(self):
"""Verify a InvalidEndpoint is received if connection times out."""
def cb(request, context):
raise requests.exceptions.Timeout
path = '/v1/images'
self.mock.get(self.endpoint + path, text=cb)
comm_err = self.assertRaises(glareclient.exc.InvalidEndpoint,
self.client.get,
'/v1/images')
self.assertIn(self.endpoint, comm_err.message)
def test_connection_refused(self):
"""Verify a CommunicationError is received if connection is refused.
The error should list the host and port that refused the connection.
"""
def cb(request, context):
raise requests.exceptions.ConnectionError()
path = '/artifacts/?limit=20'
self.mock.get(self.endpoint + path, text=cb)
comm_err = self.assertRaises(glareclient.exc.CommunicationError,
self.client.get,
'/artifacts/?limit=20')
self.assertIn(self.endpoint, comm_err.message)
def test_http_encoding(self):
path = '/artifacts/'
text = 'Ok'
self.mock.get(self.endpoint + path, text=text,
headers={"Content-Type": "text/plain"})
headers = {"test": u'ni\xf1o'}
resp, body = self.client.get(path, headers=headers)
self.assertEqual(text, resp.text)
def test_request_id(self):
path = '/artifacts/'
self.mock.get(self.endpoint + path,
headers={"x-openstack-request-id": "req-aaa"})
self.client.get(path)
self.assertEqual(self.client.last_request_id, 'req-aaa')
def test_headers_encoding(self):
value = u'ni\xf1o'
headers = {"test": value, "none-val": None}
encoded = http.encode_headers(headers)
self.assertEqual(b"ni\xc3\xb1o", encoded[b"test"])
self.assertNotIn("none-val", encoded)
def test_raw_request(self):
"""Verify the path being used for HTTP requests reflects accurately."""
headers = {"Content-Type": "text/plain"}
text = 'Ok'
path = '/artifacts/'
self.mock.get(self.endpoint + path, text=text, headers=headers)
resp, body = self.client.get('/artifacts/', headers=headers)
self.assertEqual(headers, resp.headers)
self.assertEqual(text, resp.text)
def test_parse_endpoint(self):
endpoint = 'http://example.com:9292'
test_client = http.HTTPClient(endpoint, token=u'adc123')
actual = test_client.parse_endpoint(endpoint)
expected = parse.SplitResult(scheme='http',
netloc='example.com:9292', path='',
query='', fragment='')
self.assertEqual(expected, actual)
def test_get_connections_kwargs_http(self):
endpoint = 'http://example.com:9292'
test_client = http.HTTPClient(endpoint, token=u'adc123')
self.assertEqual(600.0, test_client.timeout)
def test__chunk_body_exact_size_chunk(self):
test_client = http._BaseHTTPClient()
bytestring = b'x' * http.CHUNKSIZE
data = six.BytesIO(bytestring)
chunk = list(test_client._chunk_body(data))
self.assertEqual(1, len(chunk))
self.assertEqual([bytestring], chunk)
def test_http_chunked_request(self):
text = "Ok"
data = six.StringIO(text)
path = '/artifacts'
self.mock.post(self.endpoint + path, text=text)
headers = {"test": u'chunked_request'}
resp, body = self.client.post(path, headers=headers, data=data)
self.assertIsInstance(self.mock.last_request.body, types.GeneratorType)
self.assertEqual(text, resp.text)
def test_http_json(self):
data = {"test": "json_request"}
path = '/artifacts'
text = 'OK'
self.mock.post(self.endpoint + path, text=text)
headers = {"test": u'chunked_request'}
resp, body = self.client.post(path, headers=headers, data=data)
self.assertEqual(text, resp.text)
self.assertIsInstance(self.mock.last_request.body, six.string_types)
self.assertEqual(data, json.loads(self.mock.last_request.body))
@original_only
def test_log_http_response_with_non_ascii_char(self):
try:
response = 'Ok'
headers = {"Content-Type": "text/plain",
"test": "value1\xa5\xa6"}
fake = utils.FakeResponse(headers, six.StringIO(response))
self.client.log_http_response(fake)
except UnicodeDecodeError as e:
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
@original_only
def test_log_curl_request_with_non_ascii_char(self):
try:
headers = {'header1': 'value1\xa5\xa6'}
body = 'examplebody\xa5\xa6'
self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body,
None)
except UnicodeDecodeError as e:
self.fail("Unexpected UnicodeDecodeError exception '%s'" % e)
@original_only
@mock.patch('glareclient.common.http.LOG.debug')
def test_log_curl_request_with_body_and_header(self, mock_log):
hd_name = 'header1'
hd_val = 'value1'
headers = {hd_name: hd_val}
body = 'examplebody'
self.client.log_curl_request('GET', '/api/v1/', headers, body, None)
self.assertTrue(mock_log.called, 'LOG.debug never called')
self.assertTrue(mock_log.call_args[0],
'LOG.debug called with no arguments')
hd_regex = ".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val)
self.assertThat(mock_log.call_args[0][0],
matchers.MatchesRegex(hd_regex),
'header not found in curl command')
body_regex = ".*\s-d\s+'%s'\s.*" % body
self.assertThat(mock_log.call_args[0][0],
matchers.MatchesRegex(body_regex),
'body not found in curl command')
def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert):
headers = {'header1': 'value1'}
http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key,
cert_file=cert, cacert=cacert,
token='fake-token')
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
None)
self.assertTrue(mock_log.called, 'LOG.debug never called')
self.assertTrue(mock_log.call_args[0],
'LOG.debug called with no arguments')
needles = {'key': key, 'cert': cert, 'cacert': cacert}
for option, value in six.iteritems(needles):
if value:
regex = ".*\s--%s\s+('%s'|%s).*" % (option, value, value)
self.assertThat(mock_log.call_args[0][0],
matchers.MatchesRegex(regex),
'no --%s option in curl command' % option)
else:
regex = ".*\s--%s\s+.*" % option
self.assertThat(mock_log.call_args[0][0],
matchers.Not(matchers.MatchesRegex(regex)),
'unexpected --%s option in curl command' %
option)
@mock.patch('glareclient.common.http.LOG.debug')
def test_log_curl_request_with_all_certs(self, mock_log):
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1',
'cacert2')
@mock.patch('glareclient.common.http.LOG.debug')
def test_log_curl_request_with_some_certs(self, mock_log):
self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None)
@mock.patch('glareclient.common.http.LOG.debug')
def test_log_curl_request_with_insecure_param(self, mock_log):
headers = {'header1': 'value1'}
http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True,
token='fake-token')
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
None)
self.assertTrue(mock_log.called, 'LOG.debug never called')
self.assertTrue(mock_log.call_args[0],
'LOG.debug called with no arguments')
self.assertThat(mock_log.call_args[0][0],
matchers.MatchesRegex('.*\s-k\s.*'),
'no -k option in curl command')
@mock.patch('glareclient.common.http.LOG.debug')
def test_log_curl_request_with_token_header(self, mock_log):
fake_token = 'fake-token'
headers = {'X-Auth-Token': fake_token}
http_client_object = http.HTTPClient(self.endpoint,
identity_headers=headers)
http_client_object.log_curl_request('GET', '/api/v1/', headers, None,
None)
self.assertTrue(mock_log.called, 'LOG.debug never called')
self.assertTrue(mock_log.call_args[0],
'LOG.debug called with no arguments')
token_regex = '.*%s.*' % fake_token
self.assertThat(mock_log.call_args[0][0],
matchers.Not(matchers.MatchesRegex(token_regex)),
'token found in LOG.debug parameter')
def test_expired_token_has_changed(self):
# instantiate client with some token
fake_token = b'fake-token'
http_client = http.HTTPClient(self.endpoint,
token=fake_token)
path = '/artifacts'
self.mock.get(self.endpoint + path)
http_client.get(path)
headers = self.mock.last_request.headers
self.assertEqual(fake_token, headers['X-Auth-Token'])
# refresh the token
refreshed_token = b'refreshed-token'
http_client.auth_token = refreshed_token
http_client.get(path)
headers = self.mock.last_request.headers
self.assertEqual(refreshed_token, headers['X-Auth-Token'])
# regression check for bug 1448080
unicode_token = u'ni\xf1o'
http_client.auth_token = unicode_token
http_client.get(path)
headers = self.mock.last_request.headers
self.assertEqual(b'ni\xc3\xb1o', headers['X-Auth-Token'])

View File

@ -1,226 +0,0 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import mock
import six
import ssl
import testtools
import threading
from glareclient import Client
from glareclient import exc
from glareclient import v1
if six.PY3 is True:
import socketserver
else:
import SocketServer as socketserver
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'var'))
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
self.request.recv(1024)
response = b'somebytes'
self.request.sendall(response)
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
def get_request(self):
key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key')
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
(_sock, addr) = socketserver.TCPServer.get_request(self)
sock = ssl.wrap_socket(_sock,
certfile=cert_file,
keyfile=key_file,
ca_certs=cacert,
server_side=True,
cert_reqs=ssl.CERT_REQUIRED)
return sock, addr
class TestHTTPSVerifyCert(testtools.TestCase):
"""Check 'requests' based ssl verification occurs.
The requests library performs SSL certificate validation,
however there is still a need to check that the glare
client is properly integrated with requests so that
cert validation actually happens.
"""
def setUp(self):
# Rather than spinning up a new process, we create
# a thread to perform client/server interaction.
# This should run more quickly.
super(TestHTTPSVerifyCert, self).setUp()
server = ThreadedTCPServer(('127.0.0.1', 0),
ThreadedTCPRequestHandler)
__, self.port = server.server_address
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
@mock.patch('sys.stderr')
def test_v1_requests_cert_verification(self, __):
"""v1 regression test for bug 115260."""
port = self.port
url = 'https://0.0.0.0:%d' % port
try:
client = v1.Client(url,
insecure=False,
ssl_compression=True)
client.artifacts.get('123', type_name='sample_artifact')
self.fail('No SSL exception has been raised')
except exc.CommunicationError as e:
if 'certificate verify failed' not in e.message:
self.fail('No certificate failure message is received')
except Exception:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_cert_verification_no_compression(self, __):
"""v1 regression test for bug 115260."""
# Legacy test. Verify 'no compression' has no effect
port = self.port
url = 'https://0.0.0.0:%d' % port
try:
client = v1.Client(url,
insecure=False,
ssl_compression=False)
client.artifacts.get('123', type_name='sample_artifact')
self.fail('No SSL exception has been raised')
except exc.CommunicationError as e:
if 'certificate verify failed' not in e.message:
self.fail('No certificate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_valid_cert_verification(self, __):
"""Test absence of SSL key file."""
port = self.port
url = 'https://0.0.0.0:%d' % port
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
gc = Client('1', url,
insecure=False,
ssl_compression=True,
cacert=cacert)
gc.artifacts.get('123', type_name='sample_artifact')
except exc.CommunicationError as e:
if 'certificate verify failed' in e.message:
self.fail('Certificate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_valid_cert_verification_no_compression(self, __):
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
port = self.port
url = 'https://0.0.0.0:%d' % port
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
gc = Client('1', url,
insecure=False,
ssl_compression=False,
cacert=cacert)
gc.artifacts.get('123', type_name='sample_artifact')
except exc.CommunicationError as e:
if 'certificate verify failed' in e.message:
self.fail('Certificate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_valid_cert_no_key(self, __):
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
port = self.port
url = 'https://0.0.0.0:%d' % port
cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
gc = Client('1', url,
insecure=False,
ssl_compression=False,
cert_file=cert_file,
cacert=cacert)
gc.artifacts.get('123', type_name='sample_artifact')
except exc.CommunicationError as e:
if ('PEM lib' not in e.message):
self.fail('No appropriate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_bad_cert(self, __):
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
port = self.port
url = 'https://0.0.0.0:%d' % port
cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt')
cacert = os.path.join(TEST_VAR_DIR, 'ca.crt')
try:
gc = Client('1', url,
insecure=False,
ssl_compression=False,
cert_file=cert_file,
cacert=cacert)
gc.artifacts.get('123', type_name='sample_artifact')
except exc.CommunicationError as e:
# NOTE(dsariel)
# starting from python 2.7.8 the way to handle loading private
# keys into the SSL_CTX was changed and error message become
# similar to the one in 3.X
if (six.PY2 and 'PrivateKey' not in e.message and
'PEM lib' not in e.message or
six.PY3 and 'PEM lib' not in e.message):
self.fail('No appropriate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')
@mock.patch('sys.stderr')
def test_v1_requests_bad_ca(self, __):
"""Test VerifiedHTTPSConnection: absence of SSL key file."""
port = self.port
url = 'https://0.0.0.0:%d' % port
cacert = os.path.join(TEST_VAR_DIR, 'badca.crt')
try:
gc = Client('1', url,
insecure=False,
ssl_compression=False,
cacert=cacert)
gc.artifacts.get('123', type_name='sample_artifact')
except exc.CommunicationError as e:
# NOTE(dsariel)
# starting from python 2.7.8 the way of handling x509 certificates
# was changed (github.com/python/peps/blob/master/pep-0476.txt#L28)
# and error message become similar to the one in 3.X
if (six.PY2 and 'certificate' not in e.message and
'No such file' not in e.message or
six.PY3 and 'No such file' not in e.message):
self.fail('No appropriate failure message is received')
except Exception as e:
self.fail('Unexpected exception has been raised')

View File

@ -1,3 +1,4 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
@ -13,28 +14,22 @@
# License for the specific language governing permissions and limitations
# under the License.
from glareclient.common import http
from glareclient.common import utils
from glareclient.v1 import artifacts
from glareclient.v1 import versions
class Client(object):
"""Client for the Glare Artifact Repository v2 API.
"""Client for the Glare Artifact Repository v1 API.
:param string endpoint: A user-supplied endpoint URL for the glare
service.
:param string endpoint: A user-supplied endpoint URL for the glare service.
:param string token: Token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
:param string language_header: Set Accept-Language header to be sent in
requests to glare.
"""
def __init__(self, endpoint=None, **kwargs):
endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0)
self.http_client = http.get_http_client(endpoint=endpoint, **kwargs)
def __init__(self, endpoint, **kwargs):
"""Initialize a new client for the Glare v1 API."""
self.version = kwargs.get('version')
self.http_client = http.construct_http_client(endpoint, **kwargs)
self.artifacts = artifacts.Controller(self.http_client)
self.versions = versions.VersionController(self.http_client)

View File

@ -9,4 +9,5 @@ requests>=2.10.0 # Apache-2.0
six>=1.9.0 # MIT
oslo.utils>=3.16.0 # Apache-2.0
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=3.11.0 # Apache-2.0
osc-lib>=1.0.2 # Apache-2.0