]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Fix TwitterMixin on Python 3.
authorBen Darnell <ben@bendarnell.com>
Sat, 17 Nov 2012 20:15:31 +0000 (15:15 -0500)
committerBen Darnell <ben@bendarnell.com>
Sat, 17 Nov 2012 20:15:31 +0000 (15:15 -0500)
Also add tests, and add get_auth_http_client method to all auth mixins.

Closes #634.

tornado/auth.py
tornado/test/auth_test.py
website/sphinx/releases/next.rst

index 016c5b94ed7a5a2fb7858e3a51085f5b37d2ad8b..964534fa8b535f745444719e421af5e86f132c88 100644 (file)
@@ -95,7 +95,7 @@ class OpenIdMixin(object):
         args["openid.mode"] = u"check_authentication"
         url = self._OPENID_ENDPOINT
         if http_client is None:
-            http_client = httpclient.AsyncHTTPClient()
+            http_client = self.get_auth_http_client()
         http_client.fetch(url, self.async_callback(
             self._on_authentication_verified, callback),
             method="POST", body=urllib.urlencode(args))
@@ -208,6 +208,14 @@ class OpenIdMixin(object):
             user["claimed_id"] = claimed_id
         callback(user)
 
+    def get_auth_http_client(self):
+        """Returns the AsyncHTTPClient instance to be used for auth requests.
+
+        May be overridden by subclasses to use an http client other than
+        the default.
+        """
+        return httpclient.AsyncHTTPClient()
+
 
 class OAuthMixin(object):
     """Abstract implementation of OAuth.
@@ -232,7 +240,7 @@ class OAuthMixin(object):
         if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
             raise Exception("This service does not support oauth_callback")
         if http_client is None:
-            http_client = httpclient.AsyncHTTPClient()
+            http_client = self.get_auth_http_client()
         if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
             http_client.fetch(
                 self._oauth_request_token_url(callback_uri=callback_uri,
@@ -277,7 +285,7 @@ class OAuthMixin(object):
         if oauth_verifier:
             token["verifier"] = oauth_verifier
         if http_client is None:
-            http_client = httpclient.AsyncHTTPClient()
+            http_client = self.get_auth_http_client()
         http_client.fetch(self._oauth_access_token_url(token),
                           self.async_callback(self._on_access_token, callback))
 
@@ -394,6 +402,14 @@ class OAuthMixin(object):
         base_args["oauth_signature"] = signature
         return base_args
 
+    def get_auth_http_client(self):
+        """Returns the AsyncHTTPClient instance to be used for auth requests.
+
+        May be overridden by subclasses to use an http client other than
+        the default.
+        """
+        return httpclient.AsyncHTTPClient()
+
 
 class OAuth2Mixin(object):
     """Abstract implementation of OAuth v 2."""
@@ -471,6 +487,7 @@ class TwitterMixin(OAuthMixin):
     _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
     _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
     _OAUTH_NO_CALLBACKS = False
+    _TWITTER_BASE_URL = "http://api.twitter.com/1"
 
     def authenticate_redirect(self, callback_uri=None):
         """Just like authorize_redirect(), but auto-redirects if authorized.
@@ -478,7 +495,7 @@ class TwitterMixin(OAuthMixin):
         This is generally the right interface to use if you are using
         Twitter for single-sign on.
         """
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback(
             self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
 
@@ -525,7 +542,7 @@ class TwitterMixin(OAuthMixin):
             # usual pattern: http://search.twitter.com/search.json
             url = path
         else:
-            url = "http://api.twitter.com/1" + path + ".json"
+            url = self._TWITTER_BASE_URL + path + ".json"
         # Add the OAuth resource request signature if we have credentials
         if access_token:
             all_args = {}
@@ -538,7 +555,7 @@ class TwitterMixin(OAuthMixin):
         if args:
             url += "?" + urllib.urlencode(args)
         callback = self.async_callback(self._on_twitter_request, callback)
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         if post_args is not None:
             http.fetch(url, method="POST", body=urllib.urlencode(post_args),
                        callback=callback)
@@ -563,7 +580,7 @@ class TwitterMixin(OAuthMixin):
     def _oauth_get_user(self, access_token, callback):
         callback = self.async_callback(self._parse_user_response, callback)
         self.twitter_request(
-            "/users/show/" + access_token["screen_name"],
+            "/users/show/" + escape.native_str(access_token[b("screen_name")]),
             access_token=access_token, callback=callback)
 
     def _parse_user_response(self, callback, user):
@@ -660,7 +677,7 @@ class FriendFeedMixin(OAuthMixin):
         if args:
             url += "?" + urllib.urlencode(args)
         callback = self.async_callback(self._on_friendfeed_request, callback)
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         if post_args is not None:
             http.fetch(url, method="POST", body=urllib.urlencode(post_args),
                        callback=callback)
@@ -751,7 +768,7 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
                 break
         token = self.get_argument("openid." + oauth_ns + ".request_token", "")
         if token:
-            http = httpclient.AsyncHTTPClient()
+            http = self.get_auth_http_client()
             token = dict(key=token, secret="")
             http.fetch(self._oauth_access_token_url(token),
                        self.async_callback(self._on_access_token, callback))
@@ -907,7 +924,7 @@ class FacebookMixin(object):
         args["sig"] = self._signature(args)
         url = "http://api.facebook.com/restserver.php?" + \
             urllib.urlencode(args)
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         http.fetch(url, callback=self.async_callback(
             self._parse_response, callback))
 
@@ -953,6 +970,14 @@ class FacebookMixin(object):
             body = body.encode("utf-8")
         return hashlib.md5(body).hexdigest()
 
+    def get_auth_http_client(self):
+        """Returns the AsyncHTTPClient instance to be used for auth requests.
+
+        May be overridden by subclasses to use an http client other than
+        the default.
+        """
+        return httpclient.AsyncHTTPClient()
+
 
 class FacebookGraphMixin(OAuth2Mixin):
     """Facebook authentication using the new Graph API and OAuth2."""
@@ -987,7 +1012,7 @@ class FacebookGraphMixin(OAuth2Mixin):
                 self.finish()
 
         """
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         args = {
           "redirect_uri": redirect_uri,
           "code": code,
@@ -1081,7 +1106,7 @@ class FacebookGraphMixin(OAuth2Mixin):
         if all_args:
             url += "?" + urllib.urlencode(all_args)
         callback = self.async_callback(self._on_facebook_request, callback)
-        http = httpclient.AsyncHTTPClient()
+        http = self.get_auth_http_client()
         if post_args is not None:
             http.fetch(url, method="POST", body=urllib.urlencode(post_args),
                        callback=callback)
@@ -1096,6 +1121,14 @@ class FacebookGraphMixin(OAuth2Mixin):
             return
         callback(escape.json_decode(response.body))
 
+    def get_auth_http_client(self):
+        """Returns the AsyncHTTPClient instance to be used for auth requests.
+
+        May be overridden by subclasses to use an http client other than
+        the default.
+        """
+        return httpclient.AsyncHTTPClient()
+
 
 def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
     """Calculates the HMAC-SHA1 OAuth signature for the given request.
index 6459d4c1d836facd75de396a8155ccf0658a3069..54213986ed0bcf534d02e624d4a9ed58384998bc 100644 (file)
@@ -5,7 +5,7 @@
 
 
 from __future__ import absolute_import, division, with_statement
-from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin
+from tornado.auth import OpenIdMixin, OAuthMixin, OAuth2Mixin, TwitterMixin
 from tornado.escape import json_decode
 from tornado.testing import AsyncHTTPTestCase
 from tornado.util import b
@@ -101,6 +101,37 @@ class OAuth2ClientLoginHandler(RequestHandler, OAuth2Mixin):
         self.authorize_redirect()
 
 
+class TwitterClientLoginHandler(RequestHandler, TwitterMixin):
+    def initialize(self, test):
+        self._OAUTH_REQUEST_TOKEN_URL = test.get_url('/oauth1/server/request_token')
+        self._OAUTH_ACCESS_TOKEN_URL = test.get_url('/twitter/server/access_token')
+        self._OAUTH_AUTHORIZE_URL = test.get_url('/oauth1/server/authorize')
+        self._TWITTER_BASE_URL = test.get_url('/twitter/api')
+
+    @asynchronous
+    def get(self):
+        if self.get_argument("oauth_token", None):
+            self.get_authenticated_user(self.on_user)
+            return
+        self.authorize_redirect()
+
+    def on_user(self, user):
+        if user is None:
+            raise Exception("user is None")
+        self.finish(user)
+
+    def get_auth_http_client(self):
+        return self.settings['http_client']
+
+
+class TwitterServerAccessTokenHandler(RequestHandler):
+    def get(self):
+        self.write('oauth_token=hjkl&oauth_token_secret=vbnm&screen_name=foo')
+
+class TwitterServerShowUserHandler(RequestHandler):
+    def get(self, screen_name):
+        self.write(dict(screen_name=screen_name, name=screen_name.capitalize()))
+
 class AuthTest(AsyncHTTPTestCase):
     def get_app(self):
         return Application(
@@ -119,12 +150,19 @@ class AuthTest(AsyncHTTPTestCase):
                  dict(version='1.0a')),
                 ('/oauth2/client/login', OAuth2ClientLoginHandler, dict(test=self)),
 
+                ('/twitter/client/login', TwitterClientLoginHandler, dict(test=self)),
+
                 # simulated servers
                 ('/openid/server/authenticate', OpenIdServerAuthenticateHandler),
                 ('/oauth1/server/request_token', OAuth1ServerRequestTokenHandler),
                 ('/oauth1/server/access_token', OAuth1ServerAccessTokenHandler),
+
+                ('/twitter/server/access_token', TwitterServerAccessTokenHandler),
+                (r'/twitter/api/users/show/(.*)\.json', TwitterServerShowUserHandler),
                 ],
-            http_client=self.http_client)
+            http_client=self.http_client,
+            twitter_consumer_key='test_twitter_consumer_key',
+            twitter_consumer_secret='test_twitter_consumer_secret')
 
     def test_openid_redirect(self):
         response = self.fetch('/openid/client/login', follow_redirects=False)
@@ -198,3 +236,28 @@ class AuthTest(AsyncHTTPTestCase):
         response = self.fetch('/oauth2/client/login', follow_redirects=False)
         self.assertEqual(response.code, 302)
         self.assertTrue('/oauth2/server/authorize?' in response.headers['Location'])
+
+    def test_twitter_redirect(self):
+        # Same as test_oauth10a_redirect
+        response = self.fetch('/twitter/client/login', follow_redirects=False)
+        self.assertEqual(response.code, 302)
+        self.assertTrue(response.headers['Location'].endswith(
+            '/oauth1/server/authorize?oauth_token=zxcv'))
+        # the cookie is base64('zxcv')|base64('1234')
+        self.assertTrue(
+            '_oauth_request_token="enhjdg==|MTIzNA=="' in response.headers['Set-Cookie'],
+            response.headers['Set-Cookie'])
+
+    def test_twitter_get_user(self):
+        response = self.fetch(
+            '/twitter/client/login?oauth_token=zxcv',
+            headers={'Cookie': '_oauth_request_token=enhjdg==|MTIzNA=='})
+        response.rethrow()
+        parsed = json_decode(response.body)
+        self.assertEqual(parsed,
+                         {u'access_token': {u'key': u'hjkl',
+                                            u'screen_name': u'foo',
+                                            u'secret': u'vbnm'},
+                          u'name': u'Foo',
+                          u'screen_name': u'foo',
+                          u'username': u'foo'})
index b5043b395ca15f11cfbb33112a5b517daebb95ee..02a199c04c32eaaaf3e1d3728beb35ff5220835e 100644 (file)
@@ -159,3 +159,7 @@ In progress
 * Fixed a bug with `IOStream.read_until_close` with a ``streaming_callback``,
   which would cause some data to be passed to the final callback instead
   of the streaming callback.
+* The `tornado.auth` mixin classes now define a method
+  ``get_auth_http_client``, which can be overridden to use a non-default
+  `AsyncHTTPClient` instance (e.g. to use a different `IOLoop`)
+* `tornado.auth.TwitterMixin` now works on Python 3.