Initial commit. Move from pithos repository.
[astakos] / astakos / im / target / oauth2 / __init__.py
1 """
2 The MIT License
3
4 Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in
14 all copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 THE SOFTWARE.
23 """
24
25 import base64
26 import urllib
27 import time
28 import random
29 import urlparse
30 import hmac
31 import binascii
32 import httplib2
33
34 try:
35     from urlparse import parse_qs
36     parse_qs # placate pyflakes
37 except ImportError:
38     # fall back for Python 2.5
39     from cgi import parse_qs
40
41 try:
42     from hashlib import sha1
43     sha = sha1
44 except ImportError:
45     # hashlib was added in Python 2.5
46     import sha
47
48 import _version
49
50 __version__ = _version.__version__
51
52 OAUTH_VERSION = '1.0'  # Hi Blaine!
53 HTTP_METHOD = 'GET'
54 SIGNATURE_METHOD = 'PLAINTEXT'
55
56
57 class Error(RuntimeError):
58     """Generic exception class."""
59
60     def __init__(self, message='OAuth error occurred.'):
61         self._message = message
62
63     @property
64     def message(self):
65         """A hack to get around the deprecation errors in 2.6."""
66         return self._message
67
68     def __str__(self):
69         return self._message
70
71
72 class MissingSignature(Error):
73     pass
74
75
76 def build_authenticate_header(realm=''):
77     """Optional WWW-Authenticate header (401 error)"""
78     return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
79
80
81 def build_xoauth_string(url, consumer, token=None):
82     """Build an XOAUTH string for use in SMTP/IMPA authentication."""
83     request = Request.from_consumer_and_token(consumer, token,
84         "GET", url)
85
86     signing_method = SignatureMethod_HMAC_SHA1()
87     request.sign_request(signing_method, consumer, token)
88
89     params = []
90     for k, v in sorted(request.iteritems()):
91         if v is not None:
92             params.append('%s="%s"' % (k, escape(v)))
93
94     return "%s %s %s" % ("GET", url, ','.join(params))
95
96
97 def to_unicode(s):
98     """ Convert to unicode, raise exception with instructive error
99     message if s is not unicode, ascii, or utf-8. """
100     if not isinstance(s, unicode):
101         if not isinstance(s, str):
102             raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
103         try:
104             s = s.decode('utf-8')
105         except UnicodeDecodeError, le:
106             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,))
107     return s
108
109 def to_utf8(s):
110     return to_unicode(s).encode('utf-8')
111
112 def to_unicode_if_string(s):
113     if isinstance(s, basestring):
114         return to_unicode(s)
115     else:
116         return s
117
118 def to_utf8_if_string(s):
119     if isinstance(s, basestring):
120         return to_utf8(s)
121     else:
122         return s
123
124 def to_unicode_optional_iterator(x):
125     """
126     Raise TypeError if x is a str containing non-utf8 bytes or if x is
127     an iterable which contains such a str.
128     """
129     if isinstance(x, basestring):
130         return to_unicode(x)
131
132     try:
133         l = list(x)
134     except TypeError, e:
135         assert 'is not iterable' in str(e)
136         return x
137     else:
138         return [ to_unicode(e) for e in l ]
139
140 def to_utf8_optional_iterator(x):
141     """
142     Raise TypeError if x is a str or if x is an iterable which
143     contains a str.
144     """
145     if isinstance(x, basestring):
146         return to_utf8(x)
147
148     try:
149         l = list(x)
150     except TypeError, e:
151         assert 'is not iterable' in str(e)
152         return x
153     else:
154         return [ to_utf8_if_string(e) for e in l ]
155
156 def escape(s):
157     """Escape a URL including any /."""
158     return urllib.quote(s.encode('utf-8'), safe='~')
159
160 def generate_timestamp():
161     """Get seconds since epoch (UTC)."""
162     return int(time.time())
163
164
165 def generate_nonce(length=8):
166     """Generate pseudorandom number."""
167     return ''.join([str(random.randint(0, 9)) for i in range(length)])
168
169
170 def generate_verifier(length=8):
171     """Generate pseudorandom number."""
172     return ''.join([str(random.randint(0, 9)) for i in range(length)])
173
174
175 class Consumer(object):
176     """A consumer of OAuth-protected services.
177  
178     The OAuth consumer is a "third-party" service that wants to access
179     protected resources from an OAuth service provider on behalf of an end
180     user. It's kind of the OAuth client.
181  
182     Usually a consumer must be registered with the service provider by the
183     developer of the consumer software. As part of that process, the service
184     provider gives the consumer a *key* and a *secret* with which the consumer
185     software can identify itself to the service. The consumer will include its
186     key in each request to identify itself, but will use its secret only when
187     signing requests, to prove that the request is from that particular
188     registered consumer.
189  
190     Once registered, the consumer can then use its consumer credentials to ask
191     the service provider for a request token, kicking off the OAuth
192     authorization process.
193     """
194
195     key = None
196     secret = None
197
198     def __init__(self, key, secret):
199         self.key = key
200         self.secret = secret
201
202         if self.key is None or self.secret is None:
203             raise ValueError("Key and secret must be set.")
204
205     def __str__(self):
206         data = {'oauth_consumer_key': self.key,
207             'oauth_consumer_secret': self.secret}
208
209         return urllib.urlencode(data)
210
211
212 class Token(object):
213     """An OAuth credential used to request authorization or a protected
214     resource.
215  
216     Tokens in OAuth comprise a *key* and a *secret*. The key is included in
217     requests to identify the token being used, but the secret is used only in
218     the signature, to prove that the requester is who the server gave the
219     token to.
220  
221     When first negotiating the authorization, the consumer asks for a *request
222     token* that the live user authorizes with the service provider. The
223     consumer then exchanges the request token for an *access token* that can
224     be used to access protected resources.
225     """
226
227     key = None
228     secret = None
229     callback = None
230     callback_confirmed = None
231     verifier = None
232
233     def __init__(self, key, secret):
234         self.key = key
235         self.secret = secret
236
237         if self.key is None or self.secret is None:
238             raise ValueError("Key and secret must be set.")
239
240     def set_callback(self, callback):
241         self.callback = callback
242         self.callback_confirmed = 'true'
243
244     def set_verifier(self, verifier=None):
245         if verifier is not None:
246             self.verifier = verifier
247         else:
248             self.verifier = generate_verifier()
249
250     def get_callback_url(self):
251         if self.callback and self.verifier:
252             # Append the oauth_verifier.
253             parts = urlparse.urlparse(self.callback)
254             scheme, netloc, path, params, query, fragment = parts[:6]
255             if query:
256                 query = '%s&oauth_verifier=%s' % (query, self.verifier)
257             else:
258                 query = 'oauth_verifier=%s' % self.verifier
259             return urlparse.urlunparse((scheme, netloc, path, params,
260                 query, fragment))
261         return self.callback
262
263     def to_string(self):
264         """Returns this token as a plain string, suitable for storage.
265  
266         The resulting string includes the token's secret, so you should never
267         send or store this string where a third party can read it.
268         """
269
270         data = {
271             'oauth_token': self.key,
272             'oauth_token_secret': self.secret,
273         }
274
275         if self.callback_confirmed is not None:
276             data['oauth_callback_confirmed'] = self.callback_confirmed
277         return urllib.urlencode(data)
278  
279     @staticmethod
280     def from_string(s):
281         """Deserializes a token from a string like one returned by
282         `to_string()`."""
283
284         if not len(s):
285             raise ValueError("Invalid parameter string.")
286
287         params = parse_qs(s, keep_blank_values=False)
288         if not len(params):
289             raise ValueError("Invalid parameter string.")
290
291         try:
292             key = params['oauth_token'][0]
293         except Exception:
294             raise ValueError("'oauth_token' not found in OAuth request.")
295
296         try:
297             secret = params['oauth_token_secret'][0]
298         except Exception:
299             raise ValueError("'oauth_token_secret' not found in " 
300                 "OAuth request.")
301
302         token = Token(key, secret)
303         try:
304             token.callback_confirmed = params['oauth_callback_confirmed'][0]
305         except KeyError:
306             pass  # 1.0, no callback confirmed.
307         return token
308
309     def __str__(self):
310         return self.to_string()
311
312
313 def setter(attr):
314     name = attr.__name__
315  
316     def getter(self):
317         try:
318             return self.__dict__[name]
319         except KeyError:
320             raise AttributeError(name)
321  
322     def deleter(self):
323         del self.__dict__[name]
324  
325     return property(getter, attr, deleter)
326
327
328 class Request(dict):
329  
330     """The parameters and information for an HTTP request, suitable for
331     authorizing with OAuth credentials.
332  
333     When a consumer wants to access a service's protected resources, it does
334     so using a signed HTTP request identifying itself (the consumer) with its
335     key, and providing an access token authorized by the end user to access
336     those resources.
337  
338     """
339  
340     version = OAUTH_VERSION
341
342     def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
343                  body='', is_form_encoded=False):
344         if url is not None:
345             self.url = to_unicode(url)
346         self.method = method
347         if parameters is not None:
348             for k, v in parameters.iteritems():
349                 k = to_unicode(k)
350                 v = to_unicode_optional_iterator(v)
351                 self[k] = v
352         self.body = body
353         self.is_form_encoded = is_form_encoded
354
355
356     @setter
357     def url(self, value):
358         self.__dict__['url'] = value
359         if value is not None:
360             scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
361
362             # Exclude default port numbers.
363             if scheme == 'http' and netloc[-3:] == ':80':
364                 netloc = netloc[:-3]
365             elif scheme == 'https' and netloc[-4:] == ':443':
366                 netloc = netloc[:-4]
367             if scheme not in ('http', 'https'):
368                 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
369
370             # Normalized URL excludes params, query, and fragment.
371             self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
372         else:
373             self.normalized_url = None
374             self.__dict__['url'] = None
375  
376     @setter
377     def method(self, value):
378         self.__dict__['method'] = value.upper()
379  
380     def _get_timestamp_nonce(self):
381         return self['oauth_timestamp'], self['oauth_nonce']
382  
383     def get_nonoauth_parameters(self):
384         """Get any non-OAuth parameters."""
385         return dict([(k, v) for k, v in self.iteritems() 
386                     if not k.startswith('oauth_')])
387  
388     def to_header(self, realm=''):
389         """Serialize as a header for an HTTPAuth request."""
390         oauth_params = ((k, v) for k, v in self.items() 
391                             if k.startswith('oauth_'))
392         stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
393         header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
394         params_header = ', '.join(header_params)
395  
396         auth_header = 'OAuth realm="%s"' % realm
397         if params_header:
398             auth_header = "%s, %s" % (auth_header, params_header)
399  
400         return {'Authorization': auth_header}
401  
402     def to_postdata(self):
403         """Serialize as post data for a POST request."""
404         d = {}
405         for k, v in self.iteritems():
406             d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
407
408         # tell urlencode to deal with sequence values and map them correctly
409         # to resulting querystring. for example self["k"] = ["v1", "v2"] will
410         # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
411         return urllib.urlencode(d, True).replace('+', '%20')
412  
413     def to_url(self):
414         """Serialize as a URL for a GET request."""
415         base_url = urlparse.urlparse(self.url)
416         try:
417             query = base_url.query
418         except AttributeError:
419             # must be python <2.5
420             query = base_url[4]
421         query = parse_qs(query)
422         for k, v in self.items():
423             query.setdefault(k, []).append(v)
424         
425         try:
426             scheme = base_url.scheme
427             netloc = base_url.netloc
428             path = base_url.path
429             params = base_url.params
430             fragment = base_url.fragment
431         except AttributeError:
432             # must be python <2.5
433             scheme = base_url[0]
434             netloc = base_url[1]
435             path = base_url[2]
436             params = base_url[3]
437             fragment = base_url[5]
438         
439         url = (scheme, netloc, path, params,
440                urllib.urlencode(query, True), fragment)
441         return urlparse.urlunparse(url)
442
443     def get_parameter(self, parameter):
444         ret = self.get(parameter)
445         if ret is None:
446             raise Error('Parameter not found: %s' % parameter)
447
448         return ret
449
450     def get_normalized_parameters(self):
451         """Return a string that contains the parameters that must be signed."""
452         items = []
453         for key, value in self.iteritems():
454             if key == 'oauth_signature':
455                 continue
456             # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
457             # so we unpack sequence values into multiple items for sorting.
458             if isinstance(value, basestring):
459                 items.append((to_utf8_if_string(key), to_utf8(value)))
460             else:
461                 try:
462                     value = list(value)
463                 except TypeError, e:
464                     assert 'is not iterable' in str(e)
465                     items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
466                 else:
467                     items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
468
469         # Include any query string parameters from the provided URL
470         query = urlparse.urlparse(self.url)[4]
471
472         url_items = self._split_url_string(query).items()
473         url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
474         items.extend(url_items)
475
476         items.sort()
477         encoded_str = urllib.urlencode(items)
478         # Encode signature parameters per Oauth Core 1.0 protocol
479         # spec draft 7, section 3.6
480         # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
481         # Spaces must be encoded with "%20" instead of "+"
482         return encoded_str.replace('+', '%20').replace('%7E', '~')
483
484     def sign_request(self, signature_method, consumer, token):
485         """Set the signature parameter to the result of sign."""
486
487         if not self.is_form_encoded:
488             # according to
489             # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
490             # section 4.1.1 "OAuth Consumers MUST NOT include an
491             # oauth_body_hash parameter on requests with form-encoded
492             # request bodies."
493             self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
494
495         if 'oauth_consumer_key' not in self:
496             self['oauth_consumer_key'] = consumer.key
497
498         if token and 'oauth_token' not in self:
499             self['oauth_token'] = token.key
500
501         self['oauth_signature_method'] = signature_method.name
502         self['oauth_signature'] = signature_method.sign(self, consumer, token)
503  
504     @classmethod
505     def make_timestamp(cls):
506         """Get seconds since epoch (UTC)."""
507         return str(int(time.time()))
508  
509     @classmethod
510     def make_nonce(cls):
511         """Generate pseudorandom number."""
512         return str(random.randint(0, 100000000))
513  
514     @classmethod
515     def from_request(cls, http_method, http_url, headers=None, parameters=None,
516             query_string=None):
517         """Combines multiple parameter sources."""
518         if parameters is None:
519             parameters = {}
520  
521         # Headers
522         if headers and 'Authorization' in headers:
523             auth_header = headers['Authorization']
524             # Check that the authorization header is OAuth.
525             if auth_header[:6] == 'OAuth ':
526                 auth_header = auth_header[6:]
527                 try:
528                     # Get the parameters from the header.
529                     header_params = cls._split_header(auth_header)
530                     parameters.update(header_params)
531                 except:
532                     raise Error('Unable to parse OAuth parameters from '
533                         'Authorization header.')
534  
535         # GET or POST query string.
536         if query_string:
537             query_params = cls._split_url_string(query_string)
538             parameters.update(query_params)
539  
540         # URL parameters.
541         param_str = urlparse.urlparse(http_url)[4] # query
542         url_params = cls._split_url_string(param_str)
543         parameters.update(url_params)
544  
545         if parameters:
546             return cls(http_method, http_url, parameters)
547  
548         return None
549  
550     @classmethod
551     def from_consumer_and_token(cls, consumer, token=None,
552             http_method=HTTP_METHOD, http_url=None, parameters=None,
553             body='', is_form_encoded=False):
554         if not parameters:
555             parameters = {}
556  
557         defaults = {
558             'oauth_consumer_key': consumer.key,
559             'oauth_timestamp': cls.make_timestamp(),
560             'oauth_nonce': cls.make_nonce(),
561             'oauth_version': cls.version,
562         }
563  
564         defaults.update(parameters)
565         parameters = defaults
566  
567         if token:
568             parameters['oauth_token'] = token.key
569             if token.verifier:
570                 parameters['oauth_verifier'] = token.verifier
571  
572         return Request(http_method, http_url, parameters, body=body, 
573                        is_form_encoded=is_form_encoded)
574  
575     @classmethod
576     def from_token_and_callback(cls, token, callback=None, 
577         http_method=HTTP_METHOD, http_url=None, parameters=None):
578
579         if not parameters:
580             parameters = {}
581  
582         parameters['oauth_token'] = token.key
583  
584         if callback:
585             parameters['oauth_callback'] = callback
586  
587         return cls(http_method, http_url, parameters)
588  
589     @staticmethod
590     def _split_header(header):
591         """Turn Authorization: header into parameters."""
592         params = {}
593         parts = header.split(',')
594         for param in parts:
595             # Ignore realm parameter.
596             if param.find('realm') > -1:
597                 continue
598             # Remove whitespace.
599             param = param.strip()
600             # Split key-value.
601             param_parts = param.split('=', 1)
602             # Remove quotes and unescape the value.
603             params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
604         return params
605  
606     @staticmethod
607     def _split_url_string(param_str):
608         """Turn URL string into parameters."""
609         parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
610         for k, v in parameters.iteritems():
611             parameters[k] = urllib.unquote(v[0])
612         return parameters
613
614
615 class Client(httplib2.Http):
616     """OAuthClient is a worker to attempt to execute a request."""
617
618     def __init__(self, consumer, token=None, cache=None, timeout=None,
619         proxy_info=None):
620
621         if consumer is not None and not isinstance(consumer, Consumer):
622             raise ValueError("Invalid consumer.")
623
624         if token is not None and not isinstance(token, Token):
625             raise ValueError("Invalid token.")
626
627         self.consumer = consumer
628         self.token = token
629         self.method = SignatureMethod_HMAC_SHA1()
630
631         httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info)
632
633     def set_signature_method(self, method):
634         if not isinstance(method, SignatureMethod):
635             raise ValueError("Invalid signature method.")
636
637         self.method = method
638
639     def request(self, uri, method="GET", body='', headers=None, 
640         redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
641         DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
642
643         if not isinstance(headers, dict):
644             headers = {}
645
646         if method == "POST":
647             headers['Content-Type'] = headers.get('Content-Type', 
648                 DEFAULT_POST_CONTENT_TYPE)
649
650         is_form_encoded = \
651             headers.get('Content-Type') == 'application/x-www-form-urlencoded'
652
653         if is_form_encoded and body:
654             parameters = parse_qs(body)
655         else:
656             parameters = None
657
658         req = Request.from_consumer_and_token(self.consumer, 
659             token=self.token, http_method=method, http_url=uri, 
660             parameters=parameters, body=body, is_form_encoded=is_form_encoded)
661
662         req.sign_request(self.method, self.consumer, self.token)
663
664         schema, rest = urllib.splittype(uri)
665         if rest.startswith('//'):
666             hierpart = '//'
667         else:
668             hierpart = ''
669         host, rest = urllib.splithost(rest)
670
671         realm = schema + ':' + hierpart + host
672
673         if is_form_encoded:
674             body = req.to_postdata()
675         elif method == "GET":
676             uri = req.to_url()
677         else:
678             headers.update(req.to_header(realm=realm))
679
680         return httplib2.Http.request(self, uri, method=method, body=body,
681             headers=headers, redirections=redirections,
682             connection_type=connection_type)
683
684
685 class Server(object):
686     """A skeletal implementation of a service provider, providing protected
687     resources to requests from authorized consumers.
688  
689     This class implements the logic to check requests for authorization. You
690     can use it with your web server or web framework to protect certain
691     resources with OAuth.
692     """
693
694     timestamp_threshold = 300 # In seconds, five minutes.
695     version = OAUTH_VERSION
696     signature_methods = None
697
698     def __init__(self, signature_methods=None):
699         self.signature_methods = signature_methods or {}
700
701     def add_signature_method(self, signature_method):
702         self.signature_methods[signature_method.name] = signature_method
703         return self.signature_methods
704
705     def verify_request(self, request, consumer, token):
706         """Verifies an api call and checks all the parameters."""
707
708         self._check_version(request)
709         self._check_signature(request, consumer, token)
710         parameters = request.get_nonoauth_parameters()
711         return parameters
712
713     def build_authenticate_header(self, realm=''):
714         """Optional support for the authenticate header."""
715         return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
716
717     def _check_version(self, request):
718         """Verify the correct version of the request for this server."""
719         version = self._get_version(request)
720         if version and version != self.version:
721             raise Error('OAuth version %s not supported.' % str(version))
722
723     def _get_version(self, request):
724         """Return the version of the request for this server."""
725         try:
726             version = request.get_parameter('oauth_version')
727         except:
728             version = OAUTH_VERSION
729
730         return version
731
732     def _get_signature_method(self, request):
733         """Figure out the signature with some defaults."""
734         try:
735             signature_method = request.get_parameter('oauth_signature_method')
736         except:
737             signature_method = SIGNATURE_METHOD
738
739         try:
740             # Get the signature method object.
741             signature_method = self.signature_methods[signature_method]
742         except:
743             signature_method_names = ', '.join(self.signature_methods.keys())
744             raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
745
746         return signature_method
747
748     def _get_verifier(self, request):
749         return request.get_parameter('oauth_verifier')
750
751     def _check_signature(self, request, consumer, token):
752         timestamp, nonce = request._get_timestamp_nonce()
753         self._check_timestamp(timestamp)
754         signature_method = self._get_signature_method(request)
755
756         try:
757             signature = request.get_parameter('oauth_signature')
758         except:
759             raise MissingSignature('Missing oauth_signature.')
760
761         # Validate the signature.
762         valid = signature_method.check(request, consumer, token, signature)
763
764         if not valid:
765             key, base = signature_method.signing_base(request, consumer, token)
766
767             raise Error('Invalid signature. Expected signature base ' 
768                 'string: %s' % base)
769
770     def _check_timestamp(self, timestamp):
771         """Verify that timestamp is recentish."""
772         timestamp = int(timestamp)
773         now = int(time.time())
774         lapsed = now - timestamp
775         if lapsed > self.timestamp_threshold:
776             raise Error('Expired timestamp: given %d and now %s has a '
777                 'greater difference than threshold %d' % (timestamp, now, 
778                     self.timestamp_threshold))
779
780
781 class SignatureMethod(object):
782     """A way of signing requests.
783  
784     The OAuth protocol lets consumers and service providers pick a way to sign
785     requests. This interface shows the methods expected by the other `oauth`
786     modules for signing requests. Subclass it and implement its methods to
787     provide a new way to sign requests.
788     """
789
790     def signing_base(self, request, consumer, token):
791         """Calculates the string that needs to be signed.
792
793         This method returns a 2-tuple containing the starting key for the
794         signing and the message to be signed. The latter may be used in error
795         messages to help clients debug their software.
796
797         """
798         raise NotImplementedError
799
800     def sign(self, request, consumer, token):
801         """Returns the signature for the given request, based on the consumer
802         and token also provided.
803
804         You should use your implementation of `signing_base()` to build the
805         message to sign. Otherwise it may be less useful for debugging.
806
807         """
808         raise NotImplementedError
809
810     def check(self, request, consumer, token, signature):
811         """Returns whether the given signature is the correct signature for
812         the given consumer and token signing the given request."""
813         built = self.sign(request, consumer, token)
814         return built == signature
815
816
817 class SignatureMethod_HMAC_SHA1(SignatureMethod):
818     name = 'HMAC-SHA1'
819
820     def signing_base(self, request, consumer, token):
821         if not hasattr(request, 'normalized_url') or request.normalized_url is None:
822             raise ValueError("Base URL for request is not set.")
823
824         sig = (
825             escape(request.method),
826             escape(request.normalized_url),
827             escape(request.get_normalized_parameters()),
828         )
829
830         key = '%s&' % escape(consumer.secret)
831         if token:
832             key += escape(token.secret)
833         raw = '&'.join(sig)
834         return key, raw
835
836     def sign(self, request, consumer, token):
837         """Builds the base signature string."""
838         key, raw = self.signing_base(request, consumer, token)
839
840         hashed = hmac.new(key, raw, sha)
841
842         # Calculate the digest base 64.
843         return binascii.b2a_base64(hashed.digest())[:-1]
844
845
846 class SignatureMethod_PLAINTEXT(SignatureMethod):
847
848     name = 'PLAINTEXT'
849
850     def signing_base(self, request, consumer, token):
851         """Concatenates the consumer key and secret with the token's
852         secret."""
853         sig = '%s&' % escape(consumer.secret)
854         if token:
855             sig = sig + escape(token.secret)
856         return sig, sig
857
858     def sign(self, request, consumer, token):
859         key, raw = self.signing_base(request, consumer, token)
860         return raw