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 if isinstance(v, unicode):
403 v = v.encode("utf-8")
404 query.setdefault(k, []).append(v)
405
406 try:
407 scheme = base_url.scheme
408 netloc = base_url.netloc
409 path = base_url.path
410 params = base_url.params
411 fragment = base_url.fragment
412 except AttributeError:
413
414 scheme = base_url[0]
415 netloc = base_url[1]
416 path = base_url[2]
417 params = base_url[3]
418 fragment = base_url[5]
419
420 url = (scheme, netloc, path, params,
421 urllib.urlencode(query, True), fragment)
422 return urlparse.urlunparse(url)
423
425 ret = self.get(parameter)
426 if ret is None:
427 raise Error('Parameter not found: %s' % parameter)
428
429 return ret
430
432 """Return a string that contains the parameters that must be signed."""
433 items = []
434 for key, value in self.iteritems():
435 if key == 'oauth_signature':
436 continue
437
438
439 if isinstance(value, basestring):
440 items.append((to_utf8_if_string(key), to_utf8(value)))
441 else:
442 try:
443 value = list(value)
444 except TypeError, e:
445 assert 'is not iterable' in str(e)
446 items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
447 else:
448 items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
449
450
451 query = urlparse.urlparse(self.url)[4]
452
453 url_items = self._split_url_string(query).items()
454 url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
455 items.extend(url_items)
456
457 items.sort()
458 encoded_str = urllib.urlencode(items)
459
460
461
462
463 return encoded_str.replace('+', '%20').replace('%7E', '~')
464
466 """Set the signature parameter to the result of sign."""
467
468 if not self.is_form_encoded:
469
470
471
472
473
474 self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
475
476 if 'oauth_consumer_key' not in self:
477 self['oauth_consumer_key'] = consumer.key
478
479 if token and 'oauth_token' not in self:
480 self['oauth_token'] = token.key
481
482 self['oauth_signature_method'] = signature_method.name
483 self['oauth_signature'] = signature_method.sign(self, consumer, token)
484
485 @classmethod
487 """Get seconds since epoch (UTC)."""
488 return str(int(time.time()))
489
490 @classmethod
492 """Generate pseudorandom number."""
493 return str(random.randint(0, 100000000))
494
495 @classmethod
496 - def from_request(cls, http_method, http_url, headers=None, parameters=None,
497 query_string=None):
498 """Combines multiple parameter sources."""
499 if parameters is None:
500 parameters = {}
501
502
503 if headers and 'Authorization' in headers:
504 auth_header = headers['Authorization']
505
506 if auth_header[:6] == 'OAuth ':
507 auth_header = auth_header[6:]
508 try:
509
510 header_params = cls._split_header(auth_header)
511 parameters.update(header_params)
512 except:
513 raise Error('Unable to parse OAuth parameters from '
514 'Authorization header.')
515
516
517 if query_string:
518 query_params = cls._split_url_string(query_string)
519 parameters.update(query_params)
520
521
522 param_str = urlparse.urlparse(http_url)[4]
523 url_params = cls._split_url_string(param_str)
524 parameters.update(url_params)
525
526 if parameters:
527 return cls(http_method, http_url, parameters)
528
529 return None
530
531 @classmethod
535 if not parameters:
536 parameters = {}
537
538 defaults = {
539 'oauth_consumer_key': consumer.key,
540 'oauth_timestamp': cls.make_timestamp(),
541 'oauth_nonce': cls.make_nonce(),
542 'oauth_version': cls.version,
543 }
544
545 defaults.update(parameters)
546 parameters = defaults
547
548 if token:
549 parameters['oauth_token'] = token.key
550 if token.verifier:
551 parameters['oauth_verifier'] = token.verifier
552
553 return Request(http_method, http_url, parameters, body=body,
554 is_form_encoded=is_form_encoded)
555
556 @classmethod
559
560 if not parameters:
561 parameters = {}
562
563 parameters['oauth_token'] = token.key
564
565 if callback:
566 parameters['oauth_callback'] = callback
567
568 return cls(http_method, http_url, parameters)
569
570 @staticmethod
572 """Turn Authorization: header into parameters."""
573 params = {}
574 parts = header.split(',')
575 for param in parts:
576
577 if param.find('realm') > -1:
578 continue
579
580 param = param.strip()
581
582 param_parts = param.split('=', 1)
583
584 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
585 return params
586
587 @staticmethod
589 """Turn URL string into parameters."""
590 parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
591 for k, v in parameters.iteritems():
592 parameters[k] = urllib.unquote(v[0])
593 return parameters
594
597 """A way of signing requests.
598
599 The OAuth protocol lets consumers and service providers pick a way to sign
600 requests. This interface shows the methods expected by the other `oauth`
601 modules for signing requests. Subclass it and implement its methods to
602 provide a new way to sign requests.
603 """
604
606 """Calculates the string that needs to be signed.
607
608 This method returns a 2-tuple containing the starting key for the
609 signing and the message to be signed. The latter may be used in error
610 messages to help clients debug their software.
611
612 """
613 raise NotImplementedError
614
615 - def sign(self, request, consumer, token):
616 """Returns the signature for the given request, based on the consumer
617 and token also provided.
618
619 You should use your implementation of `signing_base()` to build the
620 message to sign. Otherwise it may be less useful for debugging.
621
622 """
623 raise NotImplementedError
624
625 - def check(self, request, consumer, token, signature):
626 """Returns whether the given signature is the correct signature for
627 the given consumer and token signing the given request."""
628 built = self.sign(request, consumer, token)
629 return built == signature
630
633 name = 'HMAC-SHA1'
634
650
651 - def sign(self, request, consumer, token):
652 """Builds the base signature string."""
653 key, raw = self.signing_base(request, consumer, token)
654
655 hashed = hmac.new(to_bytestring(key), raw, sha)
656
657
658 return binascii.b2a_base64(hashed.digest())[:-1]
659
660
661 -class SignatureMethod_PLAINTEXT(SignatureMethod):
662
663 name = 'PLAINTEXT'
664
665 - def signing_base(self, request, consumer, token):
666 """Concatenates the consumer key and secret with the token's
667 secret."""
668 sig = '%s&' % escape(consumer.secret)
669 if token:
670 sig = sig + escape(token.secret)
671 return sig, sig
672
673 - def sign(self, request, consumer, token):
674 key, raw = self.signing_base(request, consumer, token)
675 return raw
676