annotate Code/python_oauth2-master/oauth2/__init__.py @ 47:b0186d4a4496 tip

Move 7Digital dataset to Downloads
author Paulo Chiliguano <p.e.chiliguano@se14.qmul.ac.uk>
date Sat, 09 Jul 2022 00:50:43 -0500
parents e68dbee1f6db
children
rev   line source
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