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