]> projects.mako.cc - twitter-api-cdsw/blob - oauthlib/common.py
added updated version of oauthlib
[twitter-api-cdsw] / oauthlib / common.py
1 # -*- coding: utf-8 -*-
2 """
3 oauthlib.common
4 ~~~~~~~~~~~~~~
5
6 This module provides data structures and utilities common
7 to all implementations of OAuth.
8 """
9 from __future__ import absolute_import, unicode_literals
10
11 import collections
12 import datetime
13 import logging
14 import random
15 import re
16 import sys
17 import time
18
19 try:
20     from urllib import quote as _quote
21     from urllib import unquote as _unquote
22     from urllib import urlencode as _urlencode
23 except ImportError:
24     from urllib.parse import quote as _quote
25     from urllib.parse import unquote as _unquote
26     from urllib.parse import urlencode as _urlencode
27 try:
28     import urlparse
29 except ImportError:
30     import urllib.parse as urlparse
31
32 UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
33                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
34                                '0123456789')
35
36 CLIENT_ID_CHARACTER_SET = (r' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMN'
37                            'OPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}')
38
39 PASSWORD_PATTERN = re.compile(r'password=[^&]+')
40 INVALID_HEX_PATTERN = re.compile(r'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]')
41
42 always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
43                'abcdefghijklmnopqrstuvwxyz'
44                '0123456789' '_.-')
45
46 log = logging.getLogger('oauthlib')
47
48 PY3 = sys.version_info[0] == 3
49
50 if PY3:
51     unicode_type = str
52     bytes_type = bytes
53 else:
54     unicode_type = unicode
55     bytes_type = str
56
57
58 # 'safe' must be bytes (Python 2.6 requires bytes, other versions allow either)
59 def quote(s, safe=b'/'):
60     s = s.encode('utf-8') if isinstance(s, unicode_type) else s
61     s = _quote(s, safe)
62     # PY3 always returns unicode.  PY2 may return either, depending on whether
63     # it had to modify the string.
64     if isinstance(s, bytes_type):
65         s = s.decode('utf-8')
66     return s
67
68
69 def unquote(s):
70     s = _unquote(s)
71     # PY3 always returns unicode.  PY2 seems to always return what you give it,
72     # which differs from quote's behavior.  Just to be safe, make sure it is
73     # unicode before we return.
74     if isinstance(s, bytes_type):
75         s = s.decode('utf-8')
76     return s
77
78
79 def urlencode(params):
80     utf8_params = encode_params_utf8(params)
81     urlencoded = _urlencode(utf8_params)
82     if isinstance(urlencoded, unicode_type):  # PY3 returns unicode
83         return urlencoded
84     else:
85         return urlencoded.decode("utf-8")
86
87
88 def encode_params_utf8(params):
89     """Ensures that all parameters in a list of 2-element tuples are encoded to
90     bytestrings using UTF-8
91     """
92     encoded = []
93     for k, v in params:
94         encoded.append((
95             k.encode('utf-8') if isinstance(k, unicode_type) else k,
96             v.encode('utf-8') if isinstance(v, unicode_type) else v))
97     return encoded
98
99
100 def decode_params_utf8(params):
101     """Ensures that all parameters in a list of 2-element tuples are decoded to
102     unicode using UTF-8.
103     """
104     decoded = []
105     for k, v in params:
106         decoded.append((
107             k.decode('utf-8') if isinstance(k, bytes_type) else k,
108             v.decode('utf-8') if isinstance(v, bytes_type) else v))
109     return decoded
110
111
112 urlencoded = set(always_safe) | set('=&;%+~,*@!')
113
114
115 def urldecode(query):
116     """Decode a query string in x-www-form-urlencoded format into a sequence
117     of two-element tuples.
118
119     Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
120     correct formatting of the query string by validation. If validation fails
121     a ValueError will be raised. urllib.parse_qsl will only raise errors if
122     any of name-value pairs omits the equals sign.
123     """
124     # Check if query contains invalid characters
125     if query and not set(query) <= urlencoded:
126         error = ("Error trying to decode a non urlencoded string. "
127                  "Found invalid characters: %s "
128                  "in the string: '%s'. "
129                  "Please ensure the request/response body is "
130                  "x-www-form-urlencoded.")
131         raise ValueError(error % (set(query) - urlencoded, query))
132
133     # Check for correctly hex encoded values using a regular expression
134     # All encoded values begin with % followed by two hex characters
135     # correct = %00, %A0, %0A, %FF
136     # invalid = %G0, %5H, %PO
137     if INVALID_HEX_PATTERN.search(query):
138         raise ValueError('Invalid hex encoding in query string.')
139
140     # We encode to utf-8 prior to parsing because parse_qsl behaves
141     # differently on unicode input in python 2 and 3.
142     # Python 2.7
143     # >>> urlparse.parse_qsl(u'%E5%95%A6%E5%95%A6')
144     # u'\xe5\x95\xa6\xe5\x95\xa6'
145     # Python 2.7, non unicode input gives the same
146     # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6')
147     # '\xe5\x95\xa6\xe5\x95\xa6'
148     # but now we can decode it to unicode
149     # >>> urlparse.parse_qsl('%E5%95%A6%E5%95%A6').decode('utf-8')
150     # u'\u5566\u5566'
151     # Python 3.3 however
152     # >>> urllib.parse.parse_qsl(u'%E5%95%A6%E5%95%A6')
153     # u'\u5566\u5566'
154     query = query.encode(
155         'utf-8') if not PY3 and isinstance(query, unicode_type) else query
156     # We want to allow queries such as "c2" whereas urlparse.parse_qsl
157     # with the strict_parsing flag will not.
158     params = urlparse.parse_qsl(query, keep_blank_values=True)
159
160     # unicode all the things
161     return decode_params_utf8(params)
162
163
164 def extract_params(raw):
165     """Extract parameters and return them as a list of 2-tuples.
166
167     Will successfully extract parameters from urlencoded query strings,
168     dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
169     empty list of parameters. Any other input will result in a return
170     value of None.
171     """
172     if isinstance(raw, bytes_type) or isinstance(raw, unicode_type):
173         try:
174             params = urldecode(raw)
175         except ValueError:
176             params = None
177     elif hasattr(raw, '__iter__'):
178         try:
179             dict(raw)
180         except ValueError:
181             params = None
182         except TypeError:
183             params = None
184         else:
185             params = list(raw.items() if isinstance(raw, dict) else raw)
186             params = decode_params_utf8(params)
187     else:
188         params = None
189
190     return params
191
192
193 def generate_nonce():
194     """Generate pseudorandom nonce that is unlikely to repeat.
195
196     Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
197     Per `section 3.2.1`_ of the MAC Access Authentication spec.
198
199     A random 64-bit number is appended to the epoch timestamp for both
200     randomness and to decrease the likelihood of collisions.
201
202     .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
203     .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
204     """
205     return unicode_type(unicode_type(random.getrandbits(64)) + generate_timestamp())
206
207
208 def generate_timestamp():
209     """Get seconds since epoch (UTC).
210
211     Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
212     Per `section 3.2.1`_ of the MAC Access Authentication spec.
213
214     .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
215     .. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
216     """
217     return unicode_type(int(time.time()))
218
219
220 def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
221     """Generates a non-guessable OAuth token
222
223     OAuth (1 and 2) does not specify the format of tokens except that they
224     should be strings of random characters. Tokens should not be guessable
225     and entropy when generating the random characters is important. Which is
226     why SystemRandom is used instead of the default random.choice method.
227     """
228     rand = random.SystemRandom()
229     return ''.join(rand.choice(chars) for x in range(length))
230
231
232 def generate_signed_token(private_pem, request):
233     import jwt
234
235     now = datetime.datetime.utcnow()
236
237     claims = {
238         'scope': request.scope,
239         'exp': now + datetime.timedelta(seconds=request.expires_in)
240     }
241
242     claims.update(request.claims)
243
244     token = jwt.encode(claims, private_pem, 'RS256')
245     token = to_unicode(token, "UTF-8")
246
247     return token
248
249
250 def verify_signed_token(public_pem, token):
251     import jwt
252
253     return jwt.decode(token, public_pem, algorithms=['RS256'])
254
255
256 def generate_client_id(length=30, chars=CLIENT_ID_CHARACTER_SET):
257     """Generates an OAuth client_id
258
259     OAuth 2 specify the format of client_id in
260     http://tools.ietf.org/html/rfc6749#appendix-A.
261     """
262     return generate_token(length, chars)
263
264
265 def add_params_to_qs(query, params):
266     """Extend a query with a list of two-tuples."""
267     if isinstance(params, dict):
268         params = params.items()
269     queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
270     queryparams.extend(params)
271     return urlencode(queryparams)
272
273
274 def add_params_to_uri(uri, params, fragment=False):
275     """Add a list of two-tuples to the uri query components."""
276     sch, net, path, par, query, fra = urlparse.urlparse(uri)
277     if fragment:
278         fra = add_params_to_qs(fra, params)
279     else:
280         query = add_params_to_qs(query, params)
281     return urlparse.urlunparse((sch, net, path, par, query, fra))
282
283
284 def safe_string_equals(a, b):
285     """ Near-constant time string comparison.
286
287     Used in order to avoid timing attacks on sensitive information such
288     as secret keys during request verification (`rootLabs`_).
289
290     .. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
291
292     """
293     if len(a) != len(b):
294         return False
295
296     result = 0
297     for x, y in zip(a, b):
298         result |= ord(x) ^ ord(y)
299     return result == 0
300
301
302 def to_unicode(data, encoding='UTF-8'):
303     """Convert a number of different types of objects to unicode."""
304     if isinstance(data, unicode_type):
305         return data
306
307     if isinstance(data, bytes_type):
308         return unicode_type(data, encoding=encoding)
309
310     if hasattr(data, '__iter__'):
311         try:
312             dict(data)
313         except TypeError:
314             pass
315         except ValueError:
316             # Assume it's a one dimensional data structure
317             return (to_unicode(i, encoding) for i in data)
318         else:
319             # We support 2.6 which lacks dict comprehensions
320             if hasattr(data, 'items'):
321                 data = data.items()
322             return dict(((to_unicode(k, encoding), to_unicode(v, encoding)) for k, v in data))
323
324     return data
325
326
327 class CaseInsensitiveDict(dict):
328
329     """Basic case insensitive dict with strings only keys."""
330
331     proxy = {}
332
333     def __init__(self, data):
334         self.proxy = dict((k.lower(), k) for k in data)
335         for k in data:
336             self[k] = data[k]
337
338     def __contains__(self, k):
339         return k.lower() in self.proxy
340
341     def __delitem__(self, k):
342         key = self.proxy[k.lower()]
343         super(CaseInsensitiveDict, self).__delitem__(key)
344         del self.proxy[k.lower()]
345
346     def __getitem__(self, k):
347         key = self.proxy[k.lower()]
348         return super(CaseInsensitiveDict, self).__getitem__(key)
349
350     def get(self, k, default=None):
351         return self[k] if k in self else default
352
353     def __setitem__(self, k, v):
354         super(CaseInsensitiveDict, self).__setitem__(k, v)
355         self.proxy[k.lower()] = k
356
357
358 class Request(object):
359
360     """A malleable representation of a signable HTTP request.
361
362     Body argument may contain any data, but parameters will only be decoded if
363     they are one of:
364
365     * urlencoded query string
366     * dict
367     * list of 2-tuples
368
369     Anything else will be treated as raw body data to be passed through
370     unmolested.
371     """
372
373     def __init__(self, uri, http_method='GET', body=None, headers=None,
374                  encoding='utf-8'):
375         # Convert to unicode using encoding if given, else assume unicode
376         encode = lambda x: to_unicode(x, encoding) if encoding else x
377
378         self.uri = encode(uri)
379         self.http_method = encode(http_method)
380         self.headers = CaseInsensitiveDict(encode(headers or {}))
381         self.body = encode(body)
382         self.decoded_body = extract_params(self.body)
383         self.oauth_params = []
384         self.validator_log = {}
385
386         self._params = {
387             "access_token": None,
388             "client": None,
389             "client_id": None,
390             "client_secret": None,
391             "code": None,
392             "extra_credentials": None,
393             "grant_type": None,
394             "redirect_uri": None,
395             "refresh_token": None,
396             "response_type": None,
397             "scope": None,
398             "scopes": None,
399             "state": None,
400             "token": None,
401             "user": None,
402             "token_type_hint": None,
403         }
404         self._params.update(dict(urldecode(self.uri_query)))
405         self._params.update(dict(self.decoded_body or []))
406         self._params.update(self.headers)
407
408     def __getattr__(self, name):
409         if name in self._params:
410             return self._params[name]
411         else:
412             raise AttributeError(name)
413
414     def __repr__(self):
415         body = self.body
416         if body and 'password=' in body:
417             body = PASSWORD_PATTERN.sub('password=***', body)
418         return '<oauthlib.Request url="%s", http_method="%s", headers="%s", body="%s">' % (
419             self.uri, self.http_method, self.headers, body)
420
421     @property
422     def uri_query(self):
423         return urlparse.urlparse(self.uri).query
424
425     @property
426     def uri_query_params(self):
427         if not self.uri_query:
428             return []
429         return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
430                                   strict_parsing=True)
431
432     @property
433     def duplicate_params(self):
434         seen_keys = collections.defaultdict(int)
435         all_keys = (p[0]
436                     for p in (self.decoded_body or []) + self.uri_query_params)
437         for k in all_keys:
438             seen_keys[k] += 1
439         return [k for k, c in seen_keys.items() if c > 1]

Benjamin Mako Hill || Want to submit a patch?