root / snf-astakos-app / astakos / im / target / oauth2 / __init__.py @ 27993be5
History | View | Annotate | Download (28.4 kB)
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
|