diff options
| author | Idan Gazit <idan@gazit.me> | 2012-04-11 03:04:26 -0700 |
|---|---|---|
| committer | Idan Gazit <idan@gazit.me> | 2012-04-11 03:04:26 -0700 |
| commit | 03cb39d506d2698f2c3179eddc5a1a60d2b0589f (patch) | |
| tree | 17676b119058a2324f86ecd63f3c2f3efa59680b | |
| parent | 4c2f7f90b4c756b152cf9ff4fc7c413a2694e8cc (diff) | |
| parent | 0144e370874e8525b955eb007a90c205e5df7559 (diff) | |
| download | oauthlib-03cb39d506d2698f2c3179eddc5a1a60d2b0589f.tar.gz | |
Merge pull request #16 from ib-lundgren/oauth2_utils
Utility methods for OAuth2
| -rw-r--r-- | oauthlib/oauth2_draft25/utils.py | 128 | ||||
| -rw-r--r-- | tests/oauth2_draft25/test_utils.py | 57 |
2 files changed, 185 insertions, 0 deletions
diff --git a/oauthlib/oauth2_draft25/utils.py b/oauthlib/oauth2_draft25/utils.py new file mode 100644 index 0000000..8705d6f --- /dev/null +++ b/oauthlib/oauth2_draft25/utils.py @@ -0,0 +1,128 @@ +""" +oauthlib.utils +~~~~~~~~~~~~~~ + +This module contains utility methods used by various parts of the OAuth 2 spec. +""" + +import random +import string +import time +import urllib +from urlparse import urlparse, urlunparse, parse_qsl + +UNICODE_ASCII_CHARACTER_SET = (string.ascii_letters.decode('ascii') + + string.digits.decode('ascii')) + +def add_params_to_qs(query, params): + """Extend a query with a list of two-tuples. + + :param query: Query string. + :param params: List of two-tuples. + :return: extended query + """ + queryparams = parse_qsl(query, keep_blank_values=True) + queryparams.extend(params) + return urlencode(queryparams) + + +def add_params_to_uri(uri, params): + """Add a list of two-tuples to the uri query components. + + :param uri: Full URI. + :param params: List of two-tuples. + :return: uri with extended query + """ + sch, net, path, par, query, fra = urlparse(uri) + query = add_params_to_qs(query, params) + return urlunparse((sch, net, path, par, query, fra)) + + +def escape(u): + """Escape a string in an OAuth-compatible fashion. + + Per `section 3.6`_ of the spec. + + .. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6 + + """ + if not isinstance(u, unicode): + raise ValueError('Only unicode objects are escapable.') + return urllib.quote(u.encode('utf-8'), safe='~') + + +def generate_nonce(): + """Generate pseudorandom nonce that is unlikely to repeat. + + Per `section 3.2.1`_ of the MAC Access Authentication spec. + + A random 64-bit number is appended to the epoch timestamp for both + randomness and to decrease the likelihood of collisions. + + .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + """ + return unicode(unicode(random.getrandbits(64)) + generate_timestamp()) + + +def generate_timestamp(): + """Get seconds since epoch (UTC). + + Per `section 3.2.1`_ of the MAC Access Authentication spec. + + .. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1 + """ + return unicode(int(time.time())) + + +def generate_token(length=20, chars=UNICODE_ASCII_CHARACTER_SET): + """Generates a generic OAuth 2 token + + According to `section 1.4`_ and `section 1.5` of the spec, the method of token + construction is undefined. This implementation is simply a random selection + of `length` choices from `chars`. SystemRandom is used since it provides + higher entropy than random.choice. + + .. _`section 1.4`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-1.4 + .. _`section 1.5`: http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-1.5 + """ + rand = random.SystemRandom() + return u''.join(rand.choice(chars) for x in range(length)) + + +def host_from_uri(uri): + """Extract hostname and port from URI. + + Will use default port for HTTP and HTTPS if none is present in the URI. + + >>> host_from_uri(u'https://www.example.com/path?query') + u'www.example.com', u'443' + >>> host_from_uri(u'http://www.example.com:8080/path?query') + u'www.example.com', u'8080' + + :param uri: Full URI. + :param http_method: HTTP request method. + :return: hostname, port + """ + default_ports = { + u'HTTP' : u'80', + u'HTTPS' : u'443', + } + + sch, netloc, path, par, query, fra = urlparse(uri) + if u':' in netloc: + netloc, port = netloc.split(u':', 1) + else: + port = default_ports.get(sch.upper()) + + return netloc, port + + +def urlencode(query): + """Encode a sequence of two-element tuples or dictionary into a URL query string. + + Operates using an OAuth-safe escape() method, in contrast to urllib.urlenocde. + """ + # Convert dictionaries to list of tuples + if isinstance(query, dict): + query = query.items() + return "&".join(['='.join([escape(k), escape(v)]) for k, v in query]) diff --git a/tests/oauth2_draft25/test_utils.py b/tests/oauth2_draft25/test_utils.py new file mode 100644 index 0000000..6622ad4 --- /dev/null +++ b/tests/oauth2_draft25/test_utils.py @@ -0,0 +1,57 @@ +from __future__ import absolute_import + +from ..unittest import TestCase + +from oauthlib.oauth2_draft25.utils import * + + +class UtilsTests(TestCase): + + def test_escape(self): + """Assert that we are only escaping unicode""" + self.assertRaises(ValueError, escape, "I am a string type. Not a unicode type.") + self.assertEqual(escape(u"I am a unicode type."), u"I%20am%20a%20unicode%20type.") + + def test_generate_timestamp(self): + """ TODO: Better test here """ + timestamp = generate_timestamp() + self.assertTrue(isinstance(timestamp, unicode)) + self.assertTrue(int(timestamp)) + self.assertTrue(int(timestamp) > 1331672335) # is this increasing? + + def test_generate_nonce(self): + """ TODO: better test here """ + nonce = generate_nonce() + for i in range(50): + self.assertTrue(nonce != generate_nonce()) + + def test_generate_token(self): + """ TODO: better test here""" + token = generate_token() + self.assertEqual(len(token), 20) + + token = generate_token(length=44) + self.assertEqual(len(token), 44) + + token = generate_token(length=6, chars="python") + self.assertEqual(len(token), 6) + self.assertTrue("a" not in token) + + def test_host_from_uri(self): + """Test if hosts and ports are properly extracted from URIs. + + This should be done according to the MAC Authentication spec. + Defaults ports should be provided when none is present in the URI. + """ + self.assertEqual(host_from_uri(u'http://a.b-c.com:8080'), (u'a.b-c.com', u'8080')) + self.assertEqual(host_from_uri(u'https://a.b.com:8080'), (u'a.b.com', u'8080')) + self.assertEqual(host_from_uri(u'http://www.example.com'), (u'www.example.com', u'80')) + self.assertEqual(host_from_uri(u'https://www.example.com'), (u'www.example.com', u'443')) + + + + def test_urlencode(self): + """Ensure query components encoded properly""" + self.assertEqual(urlencode([(u'hello', u' world')]), u'hello=%20world') + self.assertEqual(urlencode({u'hello' : u' world'}), u'hello=%20world') + |
