Jamie Lennox 6728bf3ccb Move redirect handling to session
Particularly 305 is expected to be handled by the tests so we need to
handle this centrally if we want to have session and non-session clients
to work the same way.

Change-Id: Id4ec35ddd8b8304d24df9e6cd2ab995d123ef125
2013-12-20 03:51:50 +00:00

207 lines
7.9 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# 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 logging
import requests
import six
from keystoneclient import exceptions
from keystoneclient.openstack.common import jsonutils
USER_AGENT = 'python-keystoneclient'
_logger = logging.getLogger(__name__)
def request(url, method='GET', **kwargs):
return Session().request(url, method=method, **kwargs)
class Session(object):
user_agent = None
def __init__(self, session=None, original_ip=None, verify=True, cert=None,
timeout=None, user_agent=None):
"""Maintains client communication state and common functionality.
As much as possible the parameters to this class reflect and are passed
directly to the requests library.
:param string original_ip: The original IP of the requesting user
which will be sent to identity service in a
'Forwarded' header. (optional)
:param verify: The verification arguments to pass to requests. These
are of the same form as requests expects, so True or
False to verify (or not) against system certificates or
a path to a bundle or CA certs to check against.
(optional, defaults to True)
:param cert: A client certificate to pass to requests. These are of the
same form as requests expects. Either a single filename
containing both the certificate and key or a tuple
containing the path to the certificate then a path to the
key. (optional)
:param float timeout: A timeout to pass to requests. This should be a
numerical value indicating some amount
(or fraction) of seconds or 0 for no timeout.
(optional, defaults to 0)
:param string user_agent: A User-Agent header string to use for the
request. If not provided a default is used.
(optional, defaults to
'python-keystoneclient')
"""
if not session:
session = requests.Session()
self.session = session
self.original_ip = original_ip
self.verify = verify
self.cert = cert
self.timeout = None
if timeout is not None:
self.timeout = float(timeout)
# don't override the class variable if none provided
if user_agent is not None:
self.user_agent = user_agent
def request(self, url, method, json=None, original_ip=None,
user_agent=None, **kwargs):
"""Send an HTTP request with the specified characteristics.
Wrapper around `requests.Session.request` to handle tasks such as
setting headers, JSON encoding/decoding, and error handling.
Arguments that are not handled are passed through to the requests
library.
:param string url: Fully qualified URL of HTTP request
:param string method: The http method to use. (eg. 'GET', 'POST')
:param string original_ip: Mark this request as forwarded for this ip.
(optional)
:param dict headers: Headers to be included in the request. (optional)
:param kwargs: any other parameter that can be passed to
requests.Session.request (such as `headers`) or `json`
that will be encoded as JSON and used as `data` argument
:param json: Some data to be represented as JSON. (optional)
:param string user_agent: A user_agent to use for the request. If
present will override one present in headers.
(optional)
:raises exceptions.ClientException: For connection failure, or to
indicate an error response code.
:returns: The response to the request.
"""
headers = kwargs.setdefault('headers', dict())
if self.cert:
kwargs.setdefault('cert', self.cert)
if self.timeout is not None:
kwargs.setdefault('timeout', self.timeout)
if user_agent:
headers['User-Agent'] = user_agent
elif self.user_agent:
user_agent = headers.setdefault('User-Agent', self.user_agent)
else:
user_agent = headers.setdefault('User-Agent', USER_AGENT)
if self.original_ip:
headers.setdefault('Forwarded',
'for=%s;by=%s' % (self.original_ip, user_agent))
if json is not None:
headers['Content-Type'] = 'application/json'
kwargs['data'] = jsonutils.dumps(json)
kwargs.setdefault('verify', self.verify)
string_parts = ['curl -i']
if method:
string_parts.extend([' -X ', method])
string_parts.extend([' ', url])
if headers:
for header in six.iteritems(headers):
string_parts.append(' -H "%s: %s"' % header)
_logger.debug('REQ: %s', ''.join(string_parts))
data = kwargs.get('data')
if data:
_logger.debug('REQ BODY: %s', data)
resp = self._send_request(url, method, **kwargs)
if resp.status_code >= 400:
_logger.debug('Request returned failure status: %s',
resp.status_code)
raise exceptions.from_response(resp, method, url)
return resp
def _send_request(self, url, method, **kwargs):
try:
resp = self.session.request(method, url, **kwargs)
except requests.exceptions.SSLError:
msg = 'SSL exception connecting to %s' % url
raise exceptions.SSLError(msg)
except requests.exceptions.Timeout:
msg = 'Request to %s timed out' % url
raise exceptions.Timeout(msg)
except requests.exceptions.ConnectionError:
msg = 'Unable to establish connection to %s' % url
raise exceptions.ConnectionError(msg)
_logger.debug('RESP: [%s] %s\nRESP BODY: %s\n',
resp.status_code, resp.headers, resp.text)
# NOTE(jamielennox): The requests lib will handle the majority of
# redirections. Where it fails is when POSTs are redirected which
# is apparently something handled differently by each browser which
# requests forces us to do the most compliant way (which we don't want)
# see: https://en.wikipedia.org/wiki/Post/Redirect/Get
# Nova and other direct users don't do this. Is it still relevant?
if resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location.
return self._send_request(resp.headers['location'],
method, **kwargs)
return resp
def head(self, url, **kwargs):
return self.request(url, 'HEAD', **kwargs)
def get(self, url, **kwargs):
return self.request(url, 'GET', **kwargs)
def post(self, url, **kwargs):
return self.request(url, 'POST', **kwargs)
def put(self, url, **kwargs):
return self.request(url, 'PUT', **kwargs)
def delete(self, url, **kwargs):
return self.request(url, 'DELETE', **kwargs)
def patch(self, url, **kwargs):
return self.request(url, 'PATCH', **kwargs)