1
2
3
4
5
6 import base64
7 import urllib
8 import time
9 import random
10 import urlparse
11 import hmac
12 import binascii
13
14 try:
15 from urlparse import parse_qs, parse_qsl
16 except ImportError:
17 from cgi import parse_qs, parse_qsl
18
19 from restkit.util import to_bytestring
20
21
22 try:
23 from hashlib import sha1
24 sha = sha1
25 except ImportError:
26
27 import sha
28
29 from restkit.version import __version__
30
31 OAUTH_VERSION = '1.0'
32 HTTP_METHOD = 'GET'
33 SIGNATURE_METHOD = 'PLAINTEXT'
34
35
36 -class Error(RuntimeError):
37 """Generic exception class."""
38
39 - def __init__(self, message='OAuth error occurred.'):
41
42 @property
44 """A hack to get around the deprecation errors in 2.6."""
45 return self._message
46
49
53
56 """Optional WWW-Authenticate header (401 error)"""
57 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
58
61 """Build an XOAUTH string for use in SMTP/IMPA authentication."""
62 request = Request.from_consumer_and_token(consumer, token,
63 "GET", url)
64
65 signing_method = SignatureMethod_HMAC_SHA1()
66 request.sign_request(signing_method, consumer, token)
67
68 params = []
69 for k, v in sorted(request.iteritems()):
70 if v is not None:
71 params.append('%s="%s"' % (k, escape(v)))
72
73 return "%s %s %s" % ("GET", url, ','.join(params))
74
77 """ Convert to unicode, raise exception with instructive error
78 message if s is not unicode, ascii, or utf-8. """
79 if not isinstance(s, unicode):
80 if not isinstance(s, str):
81 raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
82 try:
83 s = s.decode('utf-8')
84 except UnicodeDecodeError, le:
85 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,))
86 return s
87
90
92 if isinstance(s, basestring):
93 return to_unicode(s)
94 else:
95 return s
96
98 if isinstance(s, basestring):
99 return to_utf8(s)
100 else:
101 return s
102
104 """
105 Raise TypeError if x is a str containing non-utf8 bytes or if x is
106 an iterable which contains such a str.
107 """
108 if isinstance(x, basestring):
109 return to_unicode(x)
110
111 try:
112 l = list(x)
113 except TypeError, e:
114 assert 'is not iterable' in str(e)
115 return x
116 else:
117 return [ to_unicode(e) for e in l ]
118
120 """
121 Raise TypeError if x is a str or if x is an iterable which
122 contains a str.
123 """
124 if isinstance(x, basestring):
125 return to_utf8(x)
126
127 try:
128 l = list(x)
129 except TypeError, e:
130 assert 'is not iterable' in str(e)
131 return x
132 else:
133 return [ to_utf8_if_string(e) for e in l ]
134
136 """Escape a URL including any /."""
137 return urllib.quote(s.encode('utf-8'), safe='~')
138
140 """Get seconds since epoch (UTC)."""
141 return int(time.time())
142
145 """Generate pseudorandom number."""
146 return ''.join([str(random.randint(0, 9)) for i in range(length)])
147
150 """Generate pseudorandom number."""
151 return ''.join([str(random.randint(0, 9)) for i in range(length)])
152
155 """A consumer of OAuth-protected services.
156
157 The OAuth consumer is a "third-party" service that wants to access
158 protected resources from an OAuth service provider on behalf of an end
159 user. It's kind of the OAuth client.
160
161 Usually a consumer must be registered with the service provider by the
162 developer of the consumer software. As part of that process, the service
163 provider gives the consumer a *key* and a *secret* with which the consumer
164 software can identify itself to the service. The consumer will include its
165 key in each request to identify itself, but will use its secret only when
166 signing requests, to prove that the request is from that particular
167 registered consumer.
168
169 Once registered, the consumer can then use its consumer credentials to ask
170 the service provider for a request token, kicking off the OAuth
171 authorization process.
172 """
173
174 key = None
175 secret = None
176
178 self.key = key
179 self.secret = secret
180
181 if self.key is None or self.secret is None:
182 raise ValueError("Key and secret must be set.")
183
185 data = {'oauth_consumer_key': self.key,
186 'oauth_consumer_secret': self.secret}
187
188 return urllib.urlencode(data)
189
192 """An OAuth credential used to request authorization or a protected
193 resource.
194
195 Tokens in OAuth comprise a *key* and a *secret*. The key is included in
196 requests to identify the token being used, but the secret is used only in
197 the signature, to prove that the requester is who the server gave the
198 token to.
199
200 When first negotiating the authorization, the consumer asks for a *request
201 token* that the live user authorizes with the service provider. The
202 consumer then exchanges the request token for an *access token* that can
203 be used to access protected resources.
204 """
205
206 key = None
207 secret = None
208 callback = None
209 callback_confirmed = None
210 verifier = None
211
213 self.key = key
214 self.secret = secret
215
216 if self.key is None or self.secret is None:
217 raise ValueError("Key and secret must be set.")
218
222
228
230 if self.callback and self.verifier:
231
232 parts = urlparse.urlparse(self.callback)
233 scheme, netloc, path, params, query, fragment = parts[:6]
234 if query:
235 query = '%s&oauth_verifier=%s' % (query, self.verifier)
236 else:
237 query = 'oauth_verifier=%s' % self.verifier
238 return urlparse.urlunparse((scheme, netloc, path, params,
239 query, fragment))
240 return self.callback
241
243 """Returns this token as a plain string, suitable for storage.
244
245 The resulting string includes the token's secret, so you should never
246 send or store this string where a third party can read it.
247 """
248
249 data = {
250 'oauth_token': self.key,
251 'oauth_token_secret': self.secret,
252 }
253
254 if self.callback_confirmed is not None:
255 data['oauth_callback_confirmed'] = self.callback_confirmed
256 return urllib.urlencode(data)
257
258 @staticmethod
260 """Deserializes a token from a string like one returned by
261 `to_string()`."""
262
263 if not len(s):
264 raise ValueError("Invalid parameter string.")
265
266 params = parse_qs(s, keep_blank_values=False)
267 if not len(params):
268 raise ValueError("Invalid parameter string.")
269
270 try:
271 key = params['oauth_token'][0]
272 except Exception:
273 raise ValueError("'oauth_token' not found in OAuth request.")
274
275 try:
276 secret = params['oauth_token_secret'][0]
277 except Exception:
278 raise ValueError("'oauth_token_secret' not found in "
279 "OAuth request.")
280
281 token = Token(key, secret)
282 try:
283 token.callback_confirmed = params['oauth_callback_confirmed'][0]
284 except KeyError:
285 pass
286 return token
287
290
293 name = attr.__name__
294
295 def getter(self):
296 try:
297 return self.__dict__[name]
298 except KeyError:
299 raise AttributeError(name)
300
301 def deleter(self):
302 del self.__dict__[name]
303
304 return property(getter, attr, deleter)
305
308
309 """The parameters and information for an HTTP request, suitable for
310 authorizing with OAuth credentials.
311
312 When a consumer wants to access a service's protected resources, it does
313 so using a signed HTTP request identifying itself (the consumer) with its
314 key, and providing an access token authorized by the end user to access
315 those resources.
316
317 """
318
319 version = OAUTH_VERSION
320
321 - def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
322 body='', is_form_encoded=False):
333
334
335 @setter
336 - def url(self, value):
337 self.__dict__['url'] = value
338 if value is not None:
339 scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
340
341
342 if scheme == 'http' and netloc[-3:] == ':80':
343 netloc = netloc[:-3]
344 elif scheme == 'https' and netloc[-4:] == ':443':
345 netloc = netloc[:-4]
346 if scheme not in ('http', 'https'):
347 raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
348
349
350 self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
351 else:
352 self.normalized_url = None
353 self.__dict__['url'] = None
354
355 @setter
357 self.__dict__['method'] = value.upper()
358
360 return self['oauth_timestamp'], self['oauth_nonce']
361
363 """Get any non-OAuth parameters."""
364 return dict([(k, v) for k, v in self.iteritems()
365 if not k.startswith('oauth_')])
366
368 """Serialize as a header for an HTTPAuth request."""
369 oauth_params = ((k, v) for k, v in self.items()
370 if k.startswith('oauth_'))
371 stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
372 header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
373 params_header = ', '.join(header_params)
374
375 auth_header = 'OAuth realm="%s"' % realm
376 if params_header:
377 auth_header = "%s, %s" % (auth_header, params_header)
378
379 return {'Authorization': auth_header}
380
381 - def to_postdata(self):
382 """Serialize as post data for a POST request."""
383 d = {}
384 for k, v in self.iteritems():
385 d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
386
387
388
389
390 return urllib.urlencode(d, True).replace('+', '%20')
391
393 """Serialize as a URL for a GET request."""
394 base_url = urlparse.urlparse(self.url)
395 try:
396 query = base_url.query
397 except AttributeError:
398
399 query = base_url[4]
400 query = parse_qs(query)
401 for k, v in self.items():
402 query.setdefault(k, []).append(v)
403
404 try:
405 scheme = base_url.scheme
406 netloc = base_url.netloc
407 path = base_url.path
408 params = base_url.params
409 fragment = base_url.fragment
410 except AttributeError:
411
412 scheme = base_url[0]
413 netloc = base_url[1]
414 path = base_url[2]
415 params = base_url[3]
416 fragment = base_url[5]
417
418 url = (scheme, netloc, path, params,
419 urllib.urlencode(query, True), fragment)
420 return urlparse.urlunparse(url)
421
423 ret = self.get(parameter)
424 if ret is None:
425 raise Error('Parameter not found: %s' % parameter)
426
427 return ret
428
430 """Return a string that contains the parameters that must be signed."""
431 items = []
432 for key, value in self.iteritems():
433 if key == 'oauth_signature':
434 continue
435
436
437 if isinstance(value, basestring):
438 items.append((to_utf8_if_string(key), to_utf8(value)))
439 else:
440 try:
441 value = list(value)
442 except TypeError, e:
443 assert 'is not iterable' in str(e)
444 items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
445 else:
446 items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
447
448
449 query = urlparse.urlparse(self.url)[4]
450
451 url_items = self._split_url_string(query).items()
452 url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
453 items.extend(url_items)
454
455 items.sort()
456 encoded_str = urllib.urlencode(items)
457
458
459
460
461 return encoded_str.replace('+', '%20').replace('%7E', '~')
462
464 """Set the signature parameter to the result of sign."""
465
466 if not self.is_form_encoded:
467
468
469
470
471
472 self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
473
474 if 'oauth_consumer_key' not in self:
475 self['oauth_consumer_key'] = consumer.key
476
477 if token and 'oauth_token' not in self:
478 self['oauth_token'] = token.key
479
480 self['oauth_signature_method'] = signature_method.name
481 self['oauth_signature'] = signature_method.sign(self, consumer, token)
482
483 @classmethod
485 """Get seconds since epoch (UTC)."""
486 return str(int(time.time()))
487
488 @classmethod
490 """Generate pseudorandom number."""
491 return str(random.randint(0, 100000000))
492
493 @classmethod
494 - def from_request(cls, http_method, http_url, headers=None, parameters=None,
495 query_string=None):
496 """Combines multiple parameter sources."""
497 if parameters is None:
498 parameters = {}
499
500
501 if headers and 'Authorization' in headers:
502 auth_header = headers['Authorization']
503
504 if auth_header[:6] == 'OAuth ':
505 auth_header = auth_header[6:]
506 try:
507
508 header_params = cls._split_header(auth_header)
509 parameters.update(header_params)
510 except:
511 raise Error('Unable to parse OAuth parameters from '
512 'Authorization header.')
513
514
515 if query_string:
516 query_params = cls._split_url_string(query_string)
517 parameters.update(query_params)
518
519
520 param_str = urlparse.urlparse(http_url)[4]
521 url_params = cls._split_url_string(param_str)
522 parameters.update(url_params)
523
524 if parameters:
525 return cls(http_method, http_url, parameters)
526
527 return None
528
529 @classmethod
533 if not parameters:
534 parameters = {}
535
536 defaults = {
537 'oauth_consumer_key': consumer.key,
538 'oauth_timestamp': cls.make_timestamp(),
539 'oauth_nonce': cls.make_nonce(),
540 'oauth_version': cls.version,
541 }
542
543 defaults.update(parameters)
544 parameters = defaults
545
546 if token:
547 parameters['oauth_token'] = token.key
548 if token.verifier:
549 parameters['oauth_verifier'] = token.verifier
550
551 return Request(http_method, http_url, parameters, body=body,
552 is_form_encoded=is_form_encoded)
553
554 @classmethod
557
558 if not parameters:
559 parameters = {}
560
561 parameters['oauth_token'] = token.key
562
563 if callback:
564 parameters['oauth_callback'] = callback
565
566 return cls(http_method, http_url, parameters)
567
568 @staticmethod
570 """Turn Authorization: header into parameters."""
571 params = {}
572 parts = header.split(',')
573 for param in parts:
574
575 if param.find('realm') > -1:
576 continue
577
578 param = param.strip()
579
580 param_parts = param.split('=', 1)
581
582 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
583 return params
584
585 @staticmethod
587 """Turn URL string into parameters."""
588 parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
589 for k, v in parameters.iteritems():
590 parameters[k] = urllib.unquote(v[0])
591 return parameters
592
595 """A way of signing requests.
596
597 The OAuth protocol lets consumers and service providers pick a way to sign
598 requests. This interface shows the methods expected by the other `oauth`
599 modules for signing requests. Subclass it and implement its methods to
600 provide a new way to sign requests.
601 """
602
604 """Calculates the string that needs to be signed.
605
606 This method returns a 2-tuple containing the starting key for the
607 signing and the message to be signed. The latter may be used in error
608 messages to help clients debug their software.
609
610 """
611 raise NotImplementedError
612
613 - def sign(self, request, consumer, token):
614 """Returns the signature for the given request, based on the consumer
615 and token also provided.
616
617 You should use your implementation of `signing_base()` to build the
618 message to sign. Otherwise it may be less useful for debugging.
619
620 """
621 raise NotImplementedError
622
623 - def check(self, request, consumer, token, signature):
624 """Returns whether the given signature is the correct signature for
625 the given consumer and token signing the given request."""
626 built = self.sign(request, consumer, token)
627 return built == signature
628
631 name = 'HMAC-SHA1'
632
648
649 - def sign(self, request, consumer, token):
650 """Builds the base signature string."""
651 key, raw = self.signing_base(request, consumer, token)
652
653 hashed = hmac.new(to_bytestring(key), raw, sha)
654
655
656 return binascii.b2a_base64(hashed.digest())[:-1]
657
658
659 -class SignatureMethod_PLAINTEXT(SignatureMethod):
660
661 name = 'PLAINTEXT'
662
663 - def signing_base(self, request, consumer, token):
664 """Concatenates the consumer key and secret with the token's
665 secret."""
666 sig = '%s&' % escape(consumer.secret)
667 if token:
668 sig = sig + escape(token.secret)
669 return sig, sig
670
671 - def sign(self, request, consumer, token):
672 key, raw = self.signing_base(request, consumer, token)
673 return raw
674