From: Ben Darnell Date: Sat, 17 Nov 2012 20:15:31 +0000 (-0500) Subject: Fix TwitterMixin on Python 3. X-Git-Tag: v3.0.0~225 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e7485f858c3df3538b6334a6d0bf3d318ec9afed;p=thirdparty%2Ftornado.git Fix TwitterMixin on Python 3. Also add tests, and add get_auth_http_client method to all auth mixins. Closes #634. --- diff --git a/tornado/auth.py b/tornado/auth.py index 016c5b94e..964534fa8 100644 --- a/tornado/auth.py +++ b/tornado/auth.py @@ -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. diff --git a/tornado/test/auth_test.py b/tornado/test/auth_test.py index 6459d4c1d..54213986e 100644 --- a/tornado/test/auth_test.py +++ b/tornado/test/auth_test.py @@ -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'}) diff --git a/website/sphinx/releases/next.rst b/website/sphinx/releases/next.rst index b5043b395..02a199c04 100644 --- a/website/sphinx/releases/next.rst +++ b/website/sphinx/releases/next.rst @@ -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.