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

Benjamin Mako Hill || Want to submit a patch?