p@21: """ p@21: The MIT License p@21: p@21: Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel p@21: p@21: Permission is hereby granted, free of charge, to any person obtaining a copy p@21: of this software and associated documentation files (the "Software"), to deal p@21: in the Software without restriction, including without limitation the rights p@21: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell p@21: copies of the Software, and to permit persons to whom the Software is p@21: furnished to do so, subject to the following conditions: p@21: p@21: The above copyright notice and this permission notice shall be included in p@21: all copies or substantial portions of the Software. p@21: p@21: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR p@21: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, p@21: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE p@21: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER p@21: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, p@21: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN p@21: THE SOFTWARE. p@21: """ p@21: p@21: import base64 p@21: import urllib p@21: import time p@21: import random p@21: import urlparse p@21: import hmac p@21: import binascii p@21: import httplib2 p@21: p@21: try: p@21: from urlparse import parse_qs p@21: parse_qs # placate pyflakes p@21: except ImportError: p@21: # fall back for Python 2.5 p@21: from cgi import parse_qs p@21: p@21: try: p@21: from hashlib import sha1 p@21: sha = sha1 p@21: except ImportError: p@21: # hashlib was added in Python 2.5 p@21: import sha p@21: p@21: import _version p@21: p@21: __version__ = _version.__version__ p@21: p@21: OAUTH_VERSION = '1.0' # Hi Blaine! p@21: HTTP_METHOD = 'GET' p@21: SIGNATURE_METHOD = 'PLAINTEXT' p@21: p@21: p@21: class Error(RuntimeError): p@21: """Generic exception class.""" p@21: p@21: def __init__(self, message='OAuth error occurred.'): p@21: self._message = message p@21: p@21: @property p@21: def message(self): p@21: """A hack to get around the deprecation errors in 2.6.""" p@21: return self._message p@21: p@21: def __str__(self): p@21: return self._message p@21: p@21: p@21: class MissingSignature(Error): p@21: pass p@21: p@21: p@21: def build_authenticate_header(realm=''): p@21: """Optional WWW-Authenticate header (401 error)""" p@21: return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} p@21: p@21: p@21: def build_xoauth_string(url, consumer, token=None): p@21: """Build an XOAUTH string for use in SMTP/IMPA authentication.""" p@21: request = Request.from_consumer_and_token(consumer, token, p@21: "GET", url) p@21: p@21: signing_method = SignatureMethod_HMAC_SHA1() p@21: request.sign_request(signing_method, consumer, token) p@21: p@21: params = [] p@21: for k, v in sorted(request.iteritems()): p@21: if v is not None: p@21: params.append('%s="%s"' % (k, escape(v))) p@21: p@21: return "%s %s %s" % ("GET", url, ','.join(params)) p@21: p@21: p@21: def to_unicode(s): p@21: """ Convert to unicode, raise exception with instructive error p@21: message if s is not unicode, ascii, or utf-8. """ p@21: if not isinstance(s, unicode): p@21: if not isinstance(s, str): p@21: raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s)) p@21: try: p@21: s = s.decode('utf-8') p@21: except UnicodeDecodeError, le: p@21: raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,)) p@21: return s p@21: p@21: def to_utf8(s): p@21: return to_unicode(s).encode('utf-8') p@21: p@21: def to_unicode_if_string(s): p@21: if isinstance(s, basestring): p@21: return to_unicode(s) p@21: else: p@21: return s p@21: p@21: def to_utf8_if_string(s): p@21: if isinstance(s, basestring): p@21: return to_utf8(s) p@21: else: p@21: return s p@21: p@21: def to_unicode_optional_iterator(x): p@21: """ p@21: Raise TypeError if x is a str containing non-utf8 bytes or if x is p@21: an iterable which contains such a str. p@21: """ p@21: if isinstance(x, basestring): p@21: return to_unicode(x) p@21: p@21: try: p@21: l = list(x) p@21: except TypeError, e: p@21: assert 'is not iterable' in str(e) p@21: return x p@21: else: p@21: return [ to_unicode(e) for e in l ] p@21: p@21: def to_utf8_optional_iterator(x): p@21: """ p@21: Raise TypeError if x is a str or if x is an iterable which p@21: contains a str. p@21: """ p@21: if isinstance(x, basestring): p@21: return to_utf8(x) p@21: p@21: try: p@21: l = list(x) p@21: except TypeError, e: p@21: assert 'is not iterable' in str(e) p@21: return x p@21: else: p@21: return [ to_utf8_if_string(e) for e in l ] p@21: p@21: def escape(s): p@21: """Escape a URL including any /.""" p@21: return urllib.quote(s.encode('utf-8'), safe='~') p@21: p@21: def generate_timestamp(): p@21: """Get seconds since epoch (UTC).""" p@21: return int(time.time()) p@21: p@21: p@21: def generate_nonce(length=8): p@21: """Generate pseudorandom number.""" p@21: return ''.join([str(random.randint(0, 9)) for i in range(length)]) p@21: p@21: p@21: def generate_verifier(length=8): p@21: """Generate pseudorandom number.""" p@21: return ''.join([str(random.randint(0, 9)) for i in range(length)]) p@21: p@21: p@21: class Consumer(object): p@21: """A consumer of OAuth-protected services. p@21: p@21: The OAuth consumer is a "third-party" service that wants to access p@21: protected resources from an OAuth service provider on behalf of an end p@21: user. It's kind of the OAuth client. p@21: p@21: Usually a consumer must be registered with the service provider by the p@21: developer of the consumer software. As part of that process, the service p@21: provider gives the consumer a *key* and a *secret* with which the consumer p@21: software can identify itself to the service. The consumer will include its p@21: key in each request to identify itself, but will use its secret only when p@21: signing requests, to prove that the request is from that particular p@21: registered consumer. p@21: p@21: Once registered, the consumer can then use its consumer credentials to ask p@21: the service provider for a request token, kicking off the OAuth p@21: authorization process. p@21: """ p@21: p@21: key = None p@21: secret = None p@21: p@21: def __init__(self, key, secret): p@21: self.key = key p@21: self.secret = secret p@21: p@21: if self.key is None or self.secret is None: p@21: raise ValueError("Key and secret must be set.") p@21: p@21: def __str__(self): p@21: data = {'oauth_consumer_key': self.key, p@21: 'oauth_consumer_secret': self.secret} p@21: p@21: return urllib.urlencode(data) p@21: p@21: p@21: class Token(object): p@21: """An OAuth credential used to request authorization or a protected p@21: resource. p@21: p@21: Tokens in OAuth comprise a *key* and a *secret*. The key is included in p@21: requests to identify the token being used, but the secret is used only in p@21: the signature, to prove that the requester is who the server gave the p@21: token to. p@21: p@21: When first negotiating the authorization, the consumer asks for a *request p@21: token* that the live user authorizes with the service provider. The p@21: consumer then exchanges the request token for an *access token* that can p@21: be used to access protected resources. p@21: """ p@21: p@21: key = None p@21: secret = None p@21: callback = None p@21: callback_confirmed = None p@21: verifier = None p@21: p@21: def __init__(self, key, secret): p@21: self.key = key p@21: self.secret = secret p@21: p@21: if self.key is None or self.secret is None: p@21: raise ValueError("Key and secret must be set.") p@21: p@21: def set_callback(self, callback): p@21: self.callback = callback p@21: self.callback_confirmed = 'true' p@21: p@21: def set_verifier(self, verifier=None): p@21: if verifier is not None: p@21: self.verifier = verifier p@21: else: p@21: self.verifier = generate_verifier() p@21: p@21: def get_callback_url(self): p@21: if self.callback and self.verifier: p@21: # Append the oauth_verifier. p@21: parts = urlparse.urlparse(self.callback) p@21: scheme, netloc, path, params, query, fragment = parts[:6] p@21: if query: p@21: query = '%s&oauth_verifier=%s' % (query, self.verifier) p@21: else: p@21: query = 'oauth_verifier=%s' % self.verifier p@21: return urlparse.urlunparse((scheme, netloc, path, params, p@21: query, fragment)) p@21: return self.callback p@21: p@21: def to_string(self): p@21: """Returns this token as a plain string, suitable for storage. p@21: p@21: The resulting string includes the token's secret, so you should never p@21: send or store this string where a third party can read it. p@21: """ p@21: p@21: data = { p@21: 'oauth_token': self.key, p@21: 'oauth_token_secret': self.secret, p@21: } p@21: p@21: if self.callback_confirmed is not None: p@21: data['oauth_callback_confirmed'] = self.callback_confirmed p@21: return urllib.urlencode(data) p@21: p@21: @staticmethod p@21: def from_string(s): p@21: """Deserializes a token from a string like one returned by p@21: `to_string()`.""" p@21: p@21: if not len(s): p@21: raise ValueError("Invalid parameter string.") p@21: p@21: params = parse_qs(s, keep_blank_values=False) p@21: if not len(params): p@21: raise ValueError("Invalid parameter string.") p@21: p@21: try: p@21: key = params['oauth_token'][0] p@21: except Exception: p@21: raise ValueError("'oauth_token' not found in OAuth request.") p@21: p@21: try: p@21: secret = params['oauth_token_secret'][0] p@21: except Exception: p@21: raise ValueError("'oauth_token_secret' not found in " p@21: "OAuth request.") p@21: p@21: token = Token(key, secret) p@21: try: p@21: token.callback_confirmed = params['oauth_callback_confirmed'][0] p@21: except KeyError: p@21: pass # 1.0, no callback confirmed. p@21: return token p@21: p@21: def __str__(self): p@21: return self.to_string() p@21: p@21: p@21: def setter(attr): p@21: name = attr.__name__ p@21: p@21: def getter(self): p@21: try: p@21: return self.__dict__[name] p@21: except KeyError: p@21: raise AttributeError(name) p@21: p@21: def deleter(self): p@21: del self.__dict__[name] p@21: p@21: return property(getter, attr, deleter) p@21: p@21: p@21: class Request(dict): p@21: p@21: """The parameters and information for an HTTP request, suitable for p@21: authorizing with OAuth credentials. p@21: p@21: When a consumer wants to access a service's protected resources, it does p@21: so using a signed HTTP request identifying itself (the consumer) with its p@21: key, and providing an access token authorized by the end user to access p@21: those resources. p@21: p@21: """ p@21: p@21: version = OAUTH_VERSION p@21: p@21: def __init__(self, method=HTTP_METHOD, url=None, parameters=None, p@21: body='', is_form_encoded=False): p@21: if url is not None: p@21: self.url = to_unicode(url) p@21: self.method = method p@21: if parameters is not None: p@21: for k, v in parameters.iteritems(): p@21: k = to_unicode(k) p@21: v = to_unicode_optional_iterator(v) p@21: self[k] = v p@21: self.body = body p@21: self.is_form_encoded = is_form_encoded p@21: p@21: p@21: @setter p@21: def url(self, value): p@21: self.__dict__['url'] = value p@21: if value is not None: p@21: scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) p@21: p@21: # Exclude default port numbers. p@21: if scheme == 'http' and netloc[-3:] == ':80': p@21: netloc = netloc[:-3] p@21: elif scheme == 'https' and netloc[-4:] == ':443': p@21: netloc = netloc[:-4] p@21: if scheme not in ('http', 'https'): p@21: raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) p@21: p@21: # Normalized URL excludes params, query, and fragment. p@21: self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) p@21: else: p@21: self.normalized_url = None p@21: self.__dict__['url'] = None p@21: p@21: @setter p@21: def method(self, value): p@21: self.__dict__['method'] = value.upper() p@21: p@21: def _get_timestamp_nonce(self): p@21: return self['oauth_timestamp'], self['oauth_nonce'] p@21: p@21: def get_nonoauth_parameters(self): p@21: """Get any non-OAuth parameters.""" p@21: return dict([(k, v) for k, v in self.iteritems() p@21: if not k.startswith('oauth_')]) p@21: p@21: def to_header(self, realm=''): p@21: """Serialize as a header for an HTTPAuth request.""" p@21: oauth_params = ((k, v) for k, v in self.items() p@21: if k.startswith('oauth_')) p@21: stringy_params = ((k, escape(str(v))) for k, v in oauth_params) p@21: header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) p@21: params_header = ', '.join(header_params) p@21: p@21: auth_header = 'OAuth realm="%s"' % realm p@21: if params_header: p@21: auth_header = "%s, %s" % (auth_header, params_header) p@21: p@21: return {'Authorization': auth_header} p@21: p@21: def to_postdata(self): p@21: """Serialize as post data for a POST request.""" p@21: d = {} p@21: for k, v in self.iteritems(): p@21: d[k.encode('utf-8')] = to_utf8_optional_iterator(v) p@21: p@21: # tell urlencode to deal with sequence values and map them correctly p@21: # to resulting querystring. for example self["k"] = ["v1", "v2"] will p@21: # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D p@21: return urllib.urlencode(d, True).replace('+', '%20') p@21: p@21: def to_url(self): p@21: """Serialize as a URL for a GET request.""" p@21: base_url = urlparse.urlparse(self.url) p@21: try: p@21: query = base_url.query p@21: except AttributeError: p@21: # must be python <2.5 p@21: query = base_url[4] p@21: query = parse_qs(query) p@21: for k, v in self.items(): p@21: query.setdefault(k, []).append(v) p@21: p@21: try: p@21: scheme = base_url.scheme p@21: netloc = base_url.netloc p@21: path = base_url.path p@21: params = base_url.params p@21: fragment = base_url.fragment p@21: except AttributeError: p@21: # must be python <2.5 p@21: scheme = base_url[0] p@21: netloc = base_url[1] p@21: path = base_url[2] p@21: params = base_url[3] p@21: fragment = base_url[5] p@21: p@21: url = (scheme, netloc, path, params, p@21: urllib.urlencode(query, True), fragment) p@21: return urlparse.urlunparse(url) p@21: p@21: def get_parameter(self, parameter): p@21: ret = self.get(parameter) p@21: if ret is None: p@21: raise Error('Parameter not found: %s' % parameter) p@21: p@21: return ret p@21: p@21: def get_normalized_parameters(self): p@21: """Return a string that contains the parameters that must be signed.""" p@21: items = [] p@21: for key, value in self.iteritems(): p@21: if key == 'oauth_signature': p@21: continue p@21: # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, p@21: # so we unpack sequence values into multiple items for sorting. p@21: if isinstance(value, basestring): p@21: items.append((to_utf8_if_string(key), to_utf8(value))) p@21: else: p@21: try: p@21: value = list(value) p@21: except TypeError, e: p@21: assert 'is not iterable' in str(e) p@21: items.append((to_utf8_if_string(key), to_utf8_if_string(value))) p@21: else: p@21: items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value) p@21: p@21: # Include any query string parameters from the provided URL p@21: query = urlparse.urlparse(self.url)[4] p@21: p@21: url_items = self._split_url_string(query).items() p@21: url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ] p@21: items.extend(url_items) p@21: p@21: items.sort() p@21: encoded_str = urllib.urlencode(items) p@21: # Encode signature parameters per Oauth Core 1.0 protocol p@21: # spec draft 7, section 3.6 p@21: # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) p@21: # Spaces must be encoded with "%20" instead of "+" p@21: return encoded_str.replace('+', '%20').replace('%7E', '~') p@21: p@21: def sign_request(self, signature_method, consumer, token): p@21: """Set the signature parameter to the result of sign.""" p@21: p@21: if not self.is_form_encoded: p@21: # according to p@21: # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html p@21: # section 4.1.1 "OAuth Consumers MUST NOT include an p@21: # oauth_body_hash parameter on requests with form-encoded p@21: # request bodies." p@21: self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest()) p@21: p@21: if 'oauth_consumer_key' not in self: p@21: self['oauth_consumer_key'] = consumer.key p@21: p@21: if token and 'oauth_token' not in self: p@21: self['oauth_token'] = token.key p@21: p@21: self['oauth_signature_method'] = signature_method.name p@21: self['oauth_signature'] = signature_method.sign(self, consumer, token) p@21: p@21: @classmethod p@21: def make_timestamp(cls): p@21: """Get seconds since epoch (UTC).""" p@21: return str(int(time.time())) p@21: p@21: @classmethod p@21: def make_nonce(cls): p@21: """Generate pseudorandom number.""" p@21: return str(random.randint(0, 100000000)) p@21: p@21: @classmethod p@21: def from_request(cls, http_method, http_url, headers=None, parameters=None, p@21: query_string=None): p@21: """Combines multiple parameter sources.""" p@21: if parameters is None: p@21: parameters = {} p@21: p@21: # Headers p@21: if headers and 'Authorization' in headers: p@21: auth_header = headers['Authorization'] p@21: # Check that the authorization header is OAuth. p@21: if auth_header[:6] == 'OAuth ': p@21: auth_header = auth_header[6:] p@21: try: p@21: # Get the parameters from the header. p@21: header_params = cls._split_header(auth_header) p@21: parameters.update(header_params) p@21: except: p@21: raise Error('Unable to parse OAuth parameters from ' p@21: 'Authorization header.') p@21: p@21: # GET or POST query string. p@21: if query_string: p@21: query_params = cls._split_url_string(query_string) p@21: parameters.update(query_params) p@21: p@21: # URL parameters. p@21: param_str = urlparse.urlparse(http_url)[4] # query p@21: url_params = cls._split_url_string(param_str) p@21: parameters.update(url_params) p@21: p@21: if parameters: p@21: return cls(http_method, http_url, parameters) p@21: p@21: return None p@21: p@21: @classmethod p@21: def from_consumer_and_token(cls, consumer, token=None, p@21: http_method=HTTP_METHOD, http_url=None, parameters=None, p@21: body='', is_form_encoded=False): p@21: if not parameters: p@21: parameters = {} p@21: p@21: defaults = { p@21: 'oauth_consumer_key': consumer.key, p@21: 'oauth_timestamp': cls.make_timestamp(), p@21: 'oauth_nonce': cls.make_nonce(), p@21: 'oauth_version': cls.version, p@21: } p@21: p@21: defaults.update(parameters) p@21: parameters = defaults p@21: p@21: if token: p@21: parameters['oauth_token'] = token.key p@21: if token.verifier: p@21: parameters['oauth_verifier'] = token.verifier p@21: p@21: return Request(http_method, http_url, parameters, body=body, p@21: is_form_encoded=is_form_encoded) p@21: p@21: @classmethod p@21: def from_token_and_callback(cls, token, callback=None, p@21: http_method=HTTP_METHOD, http_url=None, parameters=None): p@21: p@21: if not parameters: p@21: parameters = {} p@21: p@21: parameters['oauth_token'] = token.key p@21: p@21: if callback: p@21: parameters['oauth_callback'] = callback p@21: p@21: return cls(http_method, http_url, parameters) p@21: p@21: @staticmethod p@21: def _split_header(header): p@21: """Turn Authorization: header into parameters.""" p@21: params = {} p@21: parts = header.split(',') p@21: for param in parts: p@21: # Ignore realm parameter. p@21: if param.find('realm') > -1: p@21: continue p@21: # Remove whitespace. p@21: param = param.strip() p@21: # Split key-value. p@21: param_parts = param.split('=', 1) p@21: # Remove quotes and unescape the value. p@21: params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) p@21: return params p@21: p@21: @staticmethod p@21: def _split_url_string(param_str): p@21: """Turn URL string into parameters.""" p@21: parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True) p@21: for k, v in parameters.iteritems(): p@21: parameters[k] = urllib.unquote(v[0]) p@21: return parameters p@21: p@21: p@21: class Client(httplib2.Http): p@21: """OAuthClient is a worker to attempt to execute a request.""" p@21: p@21: def __init__(self, consumer, token=None, cache=None, timeout=None, p@21: proxy_info=None): p@21: p@21: if consumer is not None and not isinstance(consumer, Consumer): p@21: raise ValueError("Invalid consumer.") p@21: p@21: if token is not None and not isinstance(token, Token): p@21: raise ValueError("Invalid token.") p@21: p@21: self.consumer = consumer p@21: self.token = token p@21: self.method = SignatureMethod_HMAC_SHA1() p@21: p@21: httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info) p@21: p@21: def set_signature_method(self, method): p@21: if not isinstance(method, SignatureMethod): p@21: raise ValueError("Invalid signature method.") p@21: p@21: self.method = method p@21: p@21: def request(self, uri, method="GET", body='', headers=None, p@21: redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): p@21: DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded' p@21: p@21: if not isinstance(headers, dict): p@21: headers = {} p@21: p@21: if method == "POST": p@21: headers['Content-Type'] = headers.get('Content-Type', p@21: DEFAULT_POST_CONTENT_TYPE) p@21: p@21: is_form_encoded = \ p@21: headers.get('Content-Type') == 'application/x-www-form-urlencoded' p@21: p@21: if is_form_encoded and body: p@21: parameters = dict([(k,v[0]) for k,v in parse_qs(body).items()]) p@21: else: p@21: parameters = None p@21: p@21: req = Request.from_consumer_and_token(self.consumer, p@21: token=self.token, http_method=method, http_url=uri, p@21: parameters=parameters, body=body, is_form_encoded=is_form_encoded) p@21: p@21: req.sign_request(self.method, self.consumer, self.token) p@21: p@21: schema, rest = urllib.splittype(uri) p@21: if rest.startswith('//'): p@21: hierpart = '//' p@21: else: p@21: hierpart = '' p@21: host, rest = urllib.splithost(rest) p@21: p@21: realm = schema + ':' + hierpart + host p@21: p@21: if method == "POST" and is_form_encoded: p@21: body = req.to_postdata() p@21: elif method == "GET": p@21: uri = req.to_url() p@21: else: p@21: headers.update(req.to_header(realm=realm)) p@21: p@21: return httplib2.Http.request(self, uri, method=method, body=body, p@21: headers=headers, redirections=redirections, p@21: connection_type=connection_type) p@21: p@21: p@21: class Server(object): p@21: """A skeletal implementation of a service provider, providing protected p@21: resources to requests from authorized consumers. p@21: p@21: This class implements the logic to check requests for authorization. You p@21: can use it with your web server or web framework to protect certain p@21: resources with OAuth. p@21: """ p@21: p@21: timestamp_threshold = 300 # In seconds, five minutes. p@21: version = OAUTH_VERSION p@21: signature_methods = None p@21: p@21: def __init__(self, signature_methods=None): p@21: self.signature_methods = signature_methods or {} p@21: p@21: def add_signature_method(self, signature_method): p@21: self.signature_methods[signature_method.name] = signature_method p@21: return self.signature_methods p@21: p@21: def verify_request(self, request, consumer, token): p@21: """Verifies an api call and checks all the parameters.""" p@21: p@21: self._check_version(request) p@21: self._check_signature(request, consumer, token) p@21: parameters = request.get_nonoauth_parameters() p@21: return parameters p@21: p@21: def build_authenticate_header(self, realm=''): p@21: """Optional support for the authenticate header.""" p@21: return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} p@21: p@21: def _check_version(self, request): p@21: """Verify the correct version of the request for this server.""" p@21: version = self._get_version(request) p@21: if version and version != self.version: p@21: raise Error('OAuth version %s not supported.' % str(version)) p@21: p@21: def _get_version(self, request): p@21: """Return the version of the request for this server.""" p@21: try: p@21: version = request.get_parameter('oauth_version') p@21: except: p@21: version = OAUTH_VERSION p@21: p@21: return version p@21: p@21: def _get_signature_method(self, request): p@21: """Figure out the signature with some defaults.""" p@21: try: p@21: signature_method = request.get_parameter('oauth_signature_method') p@21: except: p@21: signature_method = SIGNATURE_METHOD p@21: p@21: try: p@21: # Get the signature method object. p@21: signature_method = self.signature_methods[signature_method] p@21: except: p@21: signature_method_names = ', '.join(self.signature_methods.keys()) p@21: raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) p@21: p@21: return signature_method p@21: p@21: def _get_verifier(self, request): p@21: return request.get_parameter('oauth_verifier') p@21: p@21: def _check_signature(self, request, consumer, token): p@21: timestamp, nonce = request._get_timestamp_nonce() p@21: self._check_timestamp(timestamp) p@21: signature_method = self._get_signature_method(request) p@21: p@21: try: p@21: signature = request.get_parameter('oauth_signature') p@21: except: p@21: raise MissingSignature('Missing oauth_signature.') p@21: p@21: # Validate the signature. p@21: valid = signature_method.check(request, consumer, token, signature) p@21: p@21: if not valid: p@21: key, base = signature_method.signing_base(request, consumer, token) p@21: p@21: raise Error('Invalid signature. Expected signature base ' p@21: 'string: %s' % base) p@21: p@21: def _check_timestamp(self, timestamp): p@21: """Verify that timestamp is recentish.""" p@21: timestamp = int(timestamp) p@21: now = int(time.time()) p@21: lapsed = now - timestamp p@21: if lapsed > self.timestamp_threshold: p@21: raise Error('Expired timestamp: given %d and now %s has a ' p@21: 'greater difference than threshold %d' % (timestamp, now, p@21: self.timestamp_threshold)) p@21: p@21: p@21: class SignatureMethod(object): p@21: """A way of signing requests. p@21: p@21: The OAuth protocol lets consumers and service providers pick a way to sign p@21: requests. This interface shows the methods expected by the other `oauth` p@21: modules for signing requests. Subclass it and implement its methods to p@21: provide a new way to sign requests. p@21: """ p@21: p@21: def signing_base(self, request, consumer, token): p@21: """Calculates the string that needs to be signed. p@21: p@21: This method returns a 2-tuple containing the starting key for the p@21: signing and the message to be signed. The latter may be used in error p@21: messages to help clients debug their software. p@21: p@21: """ p@21: raise NotImplementedError p@21: p@21: def sign(self, request, consumer, token): p@21: """Returns the signature for the given request, based on the consumer p@21: and token also provided. p@21: p@21: You should use your implementation of `signing_base()` to build the p@21: message to sign. Otherwise it may be less useful for debugging. p@21: p@21: """ p@21: raise NotImplementedError p@21: p@21: def check(self, request, consumer, token, signature): p@21: """Returns whether the given signature is the correct signature for p@21: the given consumer and token signing the given request.""" p@21: built = self.sign(request, consumer, token) p@21: return built == signature p@21: p@21: p@21: class SignatureMethod_HMAC_SHA1(SignatureMethod): p@21: name = 'HMAC-SHA1' p@21: p@21: def signing_base(self, request, consumer, token): p@21: if not hasattr(request, 'normalized_url') or request.normalized_url is None: p@21: raise ValueError("Base URL for request is not set.") p@21: p@21: sig = ( p@21: escape(request.method), p@21: escape(request.normalized_url), p@21: escape(request.get_normalized_parameters()), p@21: ) p@21: p@21: key = '%s&' % escape(consumer.secret) p@21: if token: p@21: key += escape(token.secret) p@21: raw = '&'.join(sig) p@21: return key, raw p@21: p@21: def sign(self, request, consumer, token): p@21: """Builds the base signature string.""" p@21: key, raw = self.signing_base(request, consumer, token) p@21: p@21: hashed = hmac.new(key, raw, sha) p@21: p@21: # Calculate the digest base 64. p@21: return binascii.b2a_base64(hashed.digest())[:-1] p@21: p@21: p@21: class SignatureMethod_PLAINTEXT(SignatureMethod): p@21: p@21: name = 'PLAINTEXT' p@21: p@21: def signing_base(self, request, consumer, token): p@21: """Concatenates the consumer key and secret with the token's p@21: secret.""" p@21: sig = '%s&' % escape(consumer.secret) p@21: if token: p@21: sig = sig + escape(token.secret) p@21: return sig, sig p@21: p@21: def sign(self, request, consumer, token): p@21: key, raw = self.signing_base(request, consumer, token) p@21: return raw