# -*- coding: utf-8 -*- ''' rauth.session ------------- Specially wrapped Requests' :class:`~request.sessions.Session` objects. ''' from copy import deepcopy from datetime import datetime from hashlib import sha1, md5 from random import SystemRandom from time import time from rauth.compat import parse_qsl, urljoin, urlsplit, is_basestring from rauth.oauth import HmacSha1Signature from rauth.utils import (absolute_url, CaseInsensitiveDict, OAuth1Auth, OAuth2Auth, ENTITY_METHODS, FORM_URLENCODED, get_sorted_params, OPTIONAL_OAUTH_PARAMS) from requests.sessions import Session OAUTH1_DEFAULT_TIMEOUT = OAUTH2_DEFAULT_TIMEOUT = OFLY_DEFAULT_TIMEOUT = 300.0 random = SystemRandom().random class RauthSession(Session): __attrs__ = Session.__attrs__ + ['service'] def __init__(self, service): #: A back reference to a service wrapper, if we're using one. self.service = service super(RauthSession, self).__init__() def _set_url(self, url): if self.service is not None and self.service.base_url is not None and \ not absolute_url(url): return urljoin(self.service.base_url, url) return url class OAuth1Session(RauthSession): ''' A specialized :class:`~requests.sessions.Session` object, wrapping OAuth 1.0/a logic. This object is utilized by the :class:`OAuth1Service` wrapper but can be used independently of that infrastructure. Essentially this is a loose wrapping around the standard Requests codepath. State may be tracked at this layer, especially if the instance is kept around and tracked via some unique identifier, e.g. access tokens. Things like request cookies will be preserved between requests and in fact all functionality provided by a Requests' :class:`~requests.sessions.Session` object should be exposed here. If you were to use this object by itself you could do so by instantiating it like this:: session = OAuth1Session('123', '456', access_token='321', access_token_secret='654') You now have a session object which can be used to make requests exactly as you would with a normal Requests' :class:`~requests.sessions.Session` instance. This anticipates that the standard OAuth 1.0/a flow will be modeled outside of the scope of this class. In other words, if the fully qualified flow is useful to you then this object probably need not be used directly, instead consider using :class:`OAuth1Service`. Once the session object is setup, you may start making requests:: r = session.get('http://example/com/api/resource', params={'format': 'json'}) print r.json() :param consumer_key: Client consumer key. :type consumer_key: str :param consumer_secret: Client consumer secret. :type consumer_secret: str :param access_token: Access token, defaults to `None`. :type access_token: str :param access_token_secret: Access token secret, defaults to `None`. :type access_token_secret: str :param signature: A signature producing object, defaults to :class:`rauth.oauth.HmacSha1Signature`. :type signature: :class:`rauth.oauth.Signature` :param service: A back reference to the service wrapper, defaults to `None`. :type service: :class:`rauth.Service` ''' __attrs__ = RauthSession.__attrs__ + ['consumer_key', 'consumer_secret', 'access_token', 'access_token_secret', 'signature'] VERSION = '1.0' def __init__(self, consumer_key, consumer_secret, access_token=None, access_token_secret=None, signature=None, service=None): #: Client credentials. self.consumer_key = consumer_key self.consumer_secret = consumer_secret #: Access token credentials. self.access_token = access_token self.access_token_secret = access_token_secret #: Signing method. signature = signature or HmacSha1Signature self.signature = signature() super(OAuth1Session, self).__init__(service) def request(self, method, url, header_auth=False, realm='', **req_kwargs): ''' A loose wrapper around Requests' :class:`~requests.sessions.Session` which injects OAuth 1.0/a parameters. :param method: A string representation of the HTTP method to be used. :type method: str :param url: The resource to be requested. :type url: str :param header_auth: Authentication via header, defaults to `False.` :type header_auth: bool :param realm: The auth header realm, defaults to ``""``. :type realm: str :param \*\*req_kwargs: Keyworded args to be passed down to Requests. :type \*\*req_kwargs: dict ''' req_kwargs.setdefault('headers', {}) req_kwargs['headers'] = CaseInsensitiveDict(req_kwargs['headers']) url = self._set_url(url) entity_method = method.upper() in ENTITY_METHODS if entity_method and not req_kwargs.get('files', None): req_kwargs['headers'].setdefault('Content-Type', FORM_URLENCODED) form_urlencoded = \ req_kwargs['headers'].get('Content-Type') == FORM_URLENCODED # inline string conversion if is_basestring(req_kwargs.get('params')): req_kwargs['params'] = dict(parse_qsl(req_kwargs['params'])) if is_basestring(req_kwargs.get('data')) and form_urlencoded: req_kwargs['data'] = dict(parse_qsl(req_kwargs['data'])) req_kwargs.setdefault('timeout', OAUTH1_DEFAULT_TIMEOUT) oauth_params = self._get_oauth_params(req_kwargs) # ensure we always create new instances of dictionary elements for key, value in req_kwargs.items(): if isinstance(value, dict): req_kwargs[key] = deepcopy(value) # sign the request oauth_params['oauth_signature'] = \ self.signature.sign(self.consumer_secret, self.access_token_secret, method, url, oauth_params, req_kwargs) if header_auth and 'oauth_signature' not in \ req_kwargs['headers'].get('Authorization', ''): req_kwargs['auth'] = OAuth1Auth(oauth_params, realm) elif entity_method and 'oauth_signature' not in \ (req_kwargs.get('data') or {}): req_kwargs['data'] = req_kwargs.get('data') or {} # If we have a urlencoded entity-body we should pass the OAuth # parameters on this body. However, if we do not, then we need to # pass these over the request URI, i.e. on params. # # See: # # http://tools.ietf.org/html/rfc5849#section-3.5.2 # # and: # # http://tools.ietf.org/html/rfc5849#section-3.5.3 if form_urlencoded: req_kwargs['data'].update(oauth_params) else: req_kwargs.setdefault('params', {}) req_kwargs['params'].update(oauth_params) elif 'oauth_signature' not in url: req_kwargs.setdefault('params', {}) req_kwargs['params'].update(oauth_params) return super(OAuth1Session, self).request(method, url, **req_kwargs) def _parse_optional_params(self, oauth_params, req_kwargs): ''' Parses and sets optional OAuth parameters on a request. :param oauth_param: The OAuth parameter to parse. :type oauth_param: str :param req_kwargs: The keyworded arguments passed to the request method. :type req_kwargs: dict ''' params = req_kwargs.get('params', {}) data = req_kwargs.get('data') or {} for oauth_param in OPTIONAL_OAUTH_PARAMS: if oauth_param in params: oauth_params[oauth_param] = params.pop(oauth_param) if oauth_param in data: oauth_params[oauth_param] = data.pop(oauth_param) if params: req_kwargs['params'] = params if data: req_kwargs['data'] = data def _get_oauth_params(self, req_kwargs): '''Prepares OAuth params for signing.''' oauth_params = {} oauth_params['oauth_consumer_key'] = self.consumer_key oauth_params['oauth_nonce'] = sha1( str(random()).encode('ascii')).hexdigest() oauth_params['oauth_signature_method'] = self.signature.NAME oauth_params['oauth_timestamp'] = int(time()) if self.access_token is not None: oauth_params['oauth_token'] = self.access_token oauth_params['oauth_version'] = self.VERSION self._parse_optional_params(oauth_params, req_kwargs) return oauth_params class OAuth2Session(RauthSession): ''' A specialized :class:`~requests.sessions.Session` object, wrapping OAuth 2.0 logic. This object is utilized by the :class:`OAuth2Service` wrapper but can be used independently of that infrastructure. Essentially this is a loose wrapping around the standard Requests codepath. State may be tracked at this layer, especially if the instance is kept around and tracked via some unique identifier, e.g. access token. Things like request cookies will be preserved between requests and in fact all functionality provided by a Requests' :class:`~requests.sessions.Session` object should be exposed here. If you were to use this object by itself you could do so by instantiating it like this:: session = OAuth2Session('123', '456', access_token='321') You now have a session object which can be used to make requests exactly as you would with a normal Requests :class:`~requests.sessions.Session` instance. This anticipates that the standard OAuth 2.0 flow will be modeled outside of the scope of this class. In other words, if the fully qualified flow is useful to you then this object probably need not be used directly, instead consider using :class:`OAuth2Service`. Once the session object is setup, you may start making requests:: r = session.get('https://example/com/api/resource', params={'format': 'json'}) print r.json() :param client_id: Client id, defaults to `None`. :type client_id: str :param client_secret: Client secret, defaults to `None` :type client_secret: str :param access_token: Access token, defaults to `None`. :type access_token: str :param access_token_key: The name of the access token key, defaults to `'access_token'`. :type access_token_key: str :param service: A back reference to the service wrapper, defaults to `None`. :type service: :class:`rauth.Service` :param access_token_key: The name of the access token key, defaults to `'access_token'`. :type access_token_key: str ''' __attrs__ = RauthSession.__attrs__ + ['client_id', 'client_secret', 'access_token'] def __init__(self, client_id=None, client_secret=None, access_token=None, service=None, access_token_key=None): #: Client credentials. self.client_id = client_id self.client_secret = client_secret #: Access token. self.access_token = access_token #: Access token key, e.g. 'access_token'. self.access_token_key = access_token_key or 'access_token' super(OAuth2Session, self).__init__(service) def request(self, method, url, bearer_auth=True, **req_kwargs): ''' A loose wrapper around Requests' :class:`~requests.sessions.Session` which injects OAuth 2.0 parameters. :param method: A string representation of the HTTP method to be used. :type method: str :param url: The resource to be requested. :type url: str :param bearer_auth: Whether to use Bearer Authentication or not, defaults to `True`. :type bearer_auth: bool :param \*\*req_kwargs: Keyworded args to be passed down to Requests. :type \*\*req_kwargs: dict ''' req_kwargs.setdefault('params', {}) url = self._set_url(url) if is_basestring(req_kwargs['params']): req_kwargs['params'] = dict(parse_qsl(req_kwargs['params'])) if bearer_auth and self.access_token is not None: req_kwargs['auth'] = OAuth2Auth(self.access_token) else: req_kwargs['params'].update({self.access_token_key: self.access_token}) req_kwargs.setdefault('timeout', OAUTH2_DEFAULT_TIMEOUT) return super(OAuth2Session, self).request(method, url, **req_kwargs) class OflySession(RauthSession): ''' A specialized :class:`~requests.sessions.Session` object, wrapping Ofly logic. This object is utilized by the :class:`OflyService` wrapper but can be used independently of that infrastructure. Essentially this is a loose wrapping around the standard Requests codepath. State may be tracked at this layer, especially if the instance is kept around and tracked via some unique identifier. Things like request cookies will be preserved between requests and in fact all functionality provided by a Requests' :class:`~requests.sessions.Session` object should be exposed here. If you were to use this object by itself you could do so by instantiating it like this:: session = OflySession('123', '456') You now have a session object which can be used to make requests exactly as you would with a normal Requests :class:`~requests.sessions.Session` instance. This anticipates that the standard Ofly flow will be modeled outside of the scope of this class. In other words, if the fully qualified flow is useful to you then this object probably need not be used directly, instead consider using :class:`OflyService`. Once the session object is setup, you may start making requests:: r = session.get('https://example/com/api/resource', params={'format': 'json'}) print r.json() :param app_id: The oFlyAppId, i.e. "application ID". :type app_id: str :param app_secret: The oFlyAppSecret, i.e. "shared secret". :type app_secret: str :param service: A back reference to the service wrapper, defaults to `None`. :type service: :class:`rauth.Service` ''' __attrs__ = RauthSession.__attrs__ + ['app_id', 'app_secret', 'user_id'] def __init__(self, app_id, app_secret, user_id=None, service=None): #: Client credentials. self.app_id = app_id self.app_secret = app_secret #: oFlyUserid self.user_id = user_id super(OflySession, self).__init__(service) def request(self, method, url, user_id=None, hash_meth='sha1', **req_kwargs): ''' A loose wrapper around Requests' :class:`~requests.sessions.Session` which injects Ofly parameters. :param method: A string representation of the HTTP method to be used. :type method: str :param url: The resource to be requested. :type url: str :param hash_meth: The hash method to use for signing, defaults to "sha1". :type hash_meth: str :param user_id: The oflyUserid, defaults to `None`. :type user_id: str :param \*\*req_kwargs: Keyworded args to be passed down to Requests. :type \*\*req_kwargs: dict ''' req_kwargs.setdefault('params', {}) req_kwargs.setdefault('timeout', OFLY_DEFAULT_TIMEOUT) url = self._set_url(url) user_id = user_id or self.user_id assert user_id is not None, \ 'An oflyUserid must be provided as `user_id`.' if is_basestring(req_kwargs['params']): req_kwargs['params'] = dict(parse_qsl(req_kwargs['params'])) req_kwargs['params'].update({'oflyUserid': user_id}) params = OflySession.sign(url, self.app_id, self.app_secret, hash_meth=hash_meth, **req_kwargs['params']) # NOTE: Requests can't seem to handle unicode objects, instead we can # encode a string here. req_kwargs['params'] = params if not isinstance(req_kwargs['params'], bytes): req_kwargs['params'] = req_kwargs['params'].encode('utf-8') return super(OflySession, self).request(method, url, **req_kwargs) @staticmethod def sign(url, app_id, app_secret, hash_meth='sha1', **params): ''' A signature method which generates the necessary Ofly parameters. :param app_id: The oFlyAppId, i.e. "application ID". :type app_id: str :param app_secret: The oFlyAppSecret, i.e. "shared secret". :type app_secret: str :param hash_meth: The hash method to use for signing, defaults to "sha1". :type hash_meth: str :param \*\*params: Additional parameters. :type \*\*\params: dict ''' hash_meth_str = hash_meth if hash_meth == 'sha1': hash_meth = sha1 elif hash_meth == 'md5': hash_meth = md5 else: raise TypeError('hash_meth must be one of "sha1", "md5"') now = datetime.utcnow() milliseconds = now.microsecond // 1000 time_format = '%Y-%m-%dT%H:%M:%S.{0}Z'.format(milliseconds) ofly_params = {'oflyAppId': app_id, 'oflyHashMeth': hash_meth_str.upper(), 'oflyTimestamp': now.strftime(time_format)} url_path = urlsplit(url).path signature_base_string = app_secret + url_path + '?' if len(params): signature_base_string += get_sorted_params(params) + '&' signature_base_string += get_sorted_params(ofly_params) if not isinstance(signature_base_string, bytes): signature_base_string = signature_base_string.encode('utf-8') ofly_params['oflyApiSig'] = \ hash_meth(signature_base_string).hexdigest() all_params = dict(tuple(ofly_params.items()) + tuple(params.items())) return get_sorted_params(all_params)