Tornado update

This commit is contained in:
Ruud committed 2013-02-22 23:20:16 +01:00
1 parent 3eed34c710
commit a7b78d4131
36 files changed
+1784 -1293

No files matched your search

+3 -3
View File
@@ -16,7 +16,7 @@
"""The Tornado web server and tools.""" """The Tornado web server and tools."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
# version is a human-readable version number. # version is a human-readable version number.
@@ -25,5 +25,5 @@ from __future__ import absolute_import, division, with_statement
# is zero for an official release, positive for a development branch, # is zero for an official release, positive for a development branch,
# or negative for a release candidate (after the base version number # or negative for a release candidate (after the base version number
# has been incremented) # has been incremented)
version = "2.4.post2" version = "2.4.post3"
version_info = (2, 4, 0, 2) version_info = (2, 4, 0, 3)
+171 -113
View File
@@ -44,23 +44,63 @@ Example usage for Google OpenID::
# Save the user with, e.g., set_secure_cookie() # Save the user with, e.g., set_secure_cookie()
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import base64 import base64
import binascii import binascii
import functools
import hashlib import hashlib
import hmac import hmac
import time import time
import urllib
import urlparse
import uuid import uuid
from tornado.concurrent import Future, chain_future, return_future
from tornado import gen
from tornado import httpclient from tornado import httpclient
from tornado import escape from tornado import escape
from tornado.httputil import url_concat from tornado.httputil import url_concat
from tornado.log import gen_log from tornado.log import gen_log
from tornado.util import bytes_type, b from tornado.util import bytes_type, u, unicode_type, ArgReplacer
try:
import urlparse # py2
except ImportError:
import urllib.parse as urlparse # py3
try:
import urllib.parse as urllib_parse # py3
except ImportError:
import urllib as urllib_parse # py2
class AuthError(Exception):
pass
def _auth_future_to_callback(callback, future):
try:
result = future.result()
except AuthError as e:
gen_log.warning(str(e))
result = None
callback(result)
def _auth_return_future(f):
"""Similar to tornado.concurrent.return_future, but uses the auth
module's legacy callback interface.
Note that when using this decorator the ``callback`` parameter
inside the function will actually be a future.
"""
replacer = ArgReplacer(f, 'callback')
@functools.wraps(f)
def wrapper(*args, **kwargs):
future = Future()
callback, args, kwargs = replacer.replace(future, args, kwargs)
if callback is not None:
future.add_done_callback(
functools.partial(_auth_future_to_callback, callback))
f(*args, **kwargs)
return future
return wrapper
class OpenIdMixin(object): class OpenIdMixin(object):
"""Abstract implementation of OpenID and Attribute Exchange. """Abstract implementation of OpenID and Attribute Exchange.
@@ -81,8 +121,9 @@ class OpenIdMixin(object):
""" """
callback_uri = callback_uri or self.request.uri callback_uri = callback_uri or self.request.uri
args = self._openid_args(callback_uri, ax_attrs=ax_attrs) args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)) self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
@_auth_return_future
def get_authenticated_user(self, callback, http_client=None): def get_authenticated_user(self, callback, http_client=None):
"""Fetches the authenticated user data upon redirect. """Fetches the authenticated user data upon redirect.
@@ -91,23 +132,23 @@ class OpenIdMixin(object):
methods. methods.
""" """
# Verify the OpenID response via direct request to the OP # Verify the OpenID response via direct request to the OP
args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems()) args = dict((k, v[-1]) for k, v in self.request.arguments.items())
args["openid.mode"] = u"check_authentication" args["openid.mode"] = u("check_authentication")
url = self._OPENID_ENDPOINT url = self._OPENID_ENDPOINT
if http_client is None: if http_client is None:
http_client = self.get_auth_http_client() http_client = self.get_auth_http_client()
http_client.fetch(url, self.async_callback( http_client.fetch(url, self.async_callback(
self._on_authentication_verified, callback), self._on_authentication_verified, callback),
method="POST", body=urllib.urlencode(args)) method="POST", body=urllib_parse.urlencode(args))
def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None): def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
url = urlparse.urljoin(self.request.full_url(), callback_uri) url = urlparse.urljoin(self.request.full_url(), callback_uri)
args = { args = {
"openid.ns": "http://specs.openid.net/auth/2.0", "openid.ns": "http://specs.openid.net/auth/2.0",
"openid.claimed_id": "openid.claimed_id":
"http://specs.openid.net/auth/2.0/identifier_select", "http://specs.openid.net/auth/2.0/identifier_select",
"openid.identity": "openid.identity":
"http://specs.openid.net/auth/2.0/identifier_select", "http://specs.openid.net/auth/2.0/identifier_select",
"openid.return_to": url, "openid.return_to": url,
"openid.realm": urlparse.urljoin(url, '/'), "openid.realm": urlparse.urljoin(url, '/'),
"openid.mode": "checkid_setup", "openid.mode": "checkid_setup",
@@ -124,11 +165,11 @@ class OpenIdMixin(object):
required += ["firstname", "fullname", "lastname"] required += ["firstname", "fullname", "lastname"]
args.update({ args.update({
"openid.ax.type.firstname": "openid.ax.type.firstname":
"http://axschema.org/namePerson/first", "http://axschema.org/namePerson/first",
"openid.ax.type.fullname": "openid.ax.type.fullname":
"http://axschema.org/namePerson", "http://axschema.org/namePerson",
"openid.ax.type.lastname": "openid.ax.type.lastname":
"http://axschema.org/namePerson/last", "http://axschema.org/namePerson/last",
}) })
known_attrs = { known_attrs = {
"email": "http://axschema.org/contact/email", "email": "http://axschema.org/contact/email",
@@ -142,40 +183,40 @@ class OpenIdMixin(object):
if oauth_scope: if oauth_scope:
args.update({ args.update({
"openid.ns.oauth": "openid.ns.oauth":
"http://specs.openid.net/extensions/oauth/1.0", "http://specs.openid.net/extensions/oauth/1.0",
"openid.oauth.consumer": self.request.host.split(":")[0], "openid.oauth.consumer": self.request.host.split(":")[0],
"openid.oauth.scope": oauth_scope, "openid.oauth.scope": oauth_scope,
}) })
return args return args
def _on_authentication_verified(self, callback, response): def _on_authentication_verified(self, future, response):
if response.error or b("is_valid:true") not in response.body: if response.error or b"is_valid:true" not in response.body:
gen_log.warning("Invalid OpenID response: %s", response.error or future.set_exception(AuthError(
response.body) "Invalid OpenID response: %s" % (response.error or
callback(None) response.body)))
return return
# Make sure we got back at least an email from attribute exchange # Make sure we got back at least an email from attribute exchange
ax_ns = None ax_ns = None
for name in self.request.arguments.iterkeys(): for name in self.request.arguments:
if name.startswith("openid.ns.") and \ if name.startswith("openid.ns.") and \
self.get_argument(name) == u"http://openid.net/srv/ax/1.0": self.get_argument(name) == u("http://openid.net/srv/ax/1.0"):
ax_ns = name[10:] ax_ns = name[10:]
break break
def get_ax_arg(uri): def get_ax_arg(uri):
if not ax_ns: if not ax_ns:
return u"" return u("")
prefix = "openid." + ax_ns + ".type." prefix = "openid." + ax_ns + ".type."
ax_name = None ax_name = None
for name in self.request.arguments.iterkeys(): for name in self.request.arguments.keys():
if self.get_argument(name) == uri and name.startswith(prefix): if self.get_argument(name) == uri and name.startswith(prefix):
part = name[len(prefix):] part = name[len(prefix):]
ax_name = "openid." + ax_ns + ".value." + part ax_name = "openid." + ax_ns + ".value." + part
break break
if not ax_name: if not ax_name:
return u"" return u("")
return self.get_argument(ax_name, u"") return self.get_argument(ax_name, u(""))
email = get_ax_arg("http://axschema.org/contact/email") email = get_ax_arg("http://axschema.org/contact/email")
name = get_ax_arg("http://axschema.org/namePerson") name = get_ax_arg("http://axschema.org/namePerson")
@@ -194,7 +235,7 @@ class OpenIdMixin(object):
if name: if name:
user["name"] = name user["name"] = name
elif name_parts: elif name_parts:
user["name"] = u" ".join(name_parts) user["name"] = u(" ").join(name_parts)
elif email: elif email:
user["name"] = email.split("@")[0] user["name"] = email.split("@")[0]
if email: if email:
@@ -206,7 +247,7 @@ class OpenIdMixin(object):
claimed_id = self.get_argument("openid.claimed_id", None) claimed_id = self.get_argument("openid.claimed_id", None)
if claimed_id: if claimed_id:
user["claimed_id"] = claimed_id user["claimed_id"] = claimed_id
callback(user) future.set_result(user)
def get_auth_http_client(self): def get_auth_http_client(self):
"""Returns the AsyncHTTPClient instance to be used for auth requests. """Returns the AsyncHTTPClient instance to be used for auth requests.
@@ -248,7 +289,7 @@ class OAuthMixin(object):
self.async_callback( self.async_callback(
self._on_request_token, self._on_request_token,
self._OAUTH_AUTHORIZE_URL, self._OAUTH_AUTHORIZE_URL,
callback_uri)) callback_uri))
else: else:
http_client.fetch( http_client.fetch(
self._oauth_request_token_url(), self._oauth_request_token_url(),
@@ -256,6 +297,7 @@ class OAuthMixin(object):
self._on_request_token, self._OAUTH_AUTHORIZE_URL, self._on_request_token, self._OAUTH_AUTHORIZE_URL,
callback_uri)) callback_uri))
@_auth_return_future
def get_authenticated_user(self, callback, http_client=None): def get_authenticated_user(self, callback, http_client=None):
"""Gets the OAuth authorized user and access token on callback. """Gets the OAuth authorized user and access token on callback.
@@ -267,19 +309,19 @@ class OAuthMixin(object):
to this service on behalf of the user. to this service on behalf of the user.
""" """
future = callback
request_key = escape.utf8(self.get_argument("oauth_token")) request_key = escape.utf8(self.get_argument("oauth_token"))
oauth_verifier = self.get_argument("oauth_verifier", None) oauth_verifier = self.get_argument("oauth_verifier", None)
request_cookie = self.get_cookie("_oauth_request_token") request_cookie = self.get_cookie("_oauth_request_token")
if not request_cookie: if not request_cookie:
gen_log.warning("Missing OAuth request token cookie") future.set_exception(AuthError(
callback(None) "Missing OAuth request token cookie"))
return return
self.clear_cookie("_oauth_request_token") self.clear_cookie("_oauth_request_token")
cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")] cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i)) for i in request_cookie.split("|")]
if cookie_key != request_key: if cookie_key != request_key:
gen_log.info((cookie_key, request_key, request_cookie)) future.set_exception(AuthError(
gen_log.warning("Request token does not match cookie") "Request token does not match cookie"))
callback(None)
return return
token = dict(key=cookie_key, secret=cookie_secret) token = dict(key=cookie_key, secret=cookie_secret)
if oauth_verifier: if oauth_verifier:
@@ -312,23 +354,23 @@ class OAuthMixin(object):
signature = _oauth_signature(consumer_token, "GET", url, args) signature = _oauth_signature(consumer_token, "GET", url, args)
args["oauth_signature"] = signature args["oauth_signature"] = signature
return url + "?" + urllib.urlencode(args) return url + "?" + urllib_parse.urlencode(args)
def _on_request_token(self, authorize_url, callback_uri, response): def _on_request_token(self, authorize_url, callback_uri, response):
if response.error: if response.error:
raise Exception("Could not get request token") raise Exception("Could not get request token")
request_token = _oauth_parse_response(response.body) request_token = _oauth_parse_response(response.body)
data = (base64.b64encode(request_token["key"]) + b("|") + data = (base64.b64encode(request_token["key"]) + b"|" +
base64.b64encode(request_token["secret"])) base64.b64encode(request_token["secret"]))
self.set_cookie("_oauth_request_token", data) self.set_cookie("_oauth_request_token", data)
args = dict(oauth_token=request_token["key"]) args = dict(oauth_token=request_token["key"])
if callback_uri == "oob": if callback_uri == "oob":
self.finish(authorize_url + "?" + urllib.urlencode(args)) self.finish(authorize_url + "?" + urllib_parse.urlencode(args))
return return
elif callback_uri: elif callback_uri:
args["oauth_callback"] = urlparse.urljoin( args["oauth_callback"] = urlparse.urljoin(
self.request.full_url(), callback_uri) self.request.full_url(), callback_uri)
self.redirect(authorize_url + "?" + urllib.urlencode(args)) self.redirect(authorize_url + "?" + urllib_parse.urlencode(args))
def _oauth_access_token_url(self, request_token): def _oauth_access_token_url(self, request_token):
consumer_token = self._oauth_consumer_token() consumer_token = self._oauth_consumer_token()
@@ -352,27 +394,36 @@ class OAuthMixin(object):
request_token) request_token)
args["oauth_signature"] = signature args["oauth_signature"] = signature
return url + "?" + urllib.urlencode(args) return url + "?" + urllib_parse.urlencode(args)
def _on_access_token(self, callback, response): def _on_access_token(self, future, response):
if response.error: if response.error:
gen_log.warning("Could not fetch access token") future.set_exception(AuthError("Could not fetch access token"))
callback(None)
return return
access_token = _oauth_parse_response(response.body) access_token = _oauth_parse_response(response.body)
self._oauth_get_user(access_token, self.async_callback( self._oauth_get_user_future(access_token).add_done_callback(
self._on_oauth_get_user, access_token, callback)) self.async_callback(self._on_oauth_get_user, access_token, future))
@return_future
def _oauth_get_user_future(self, access_token, callback):
# By default, call the old-style _oauth_get_user, but new code
# should override this method instead.
self._oauth_get_user(access_token, callback)
def _oauth_get_user(self, access_token, callback): def _oauth_get_user(self, access_token, callback):
raise NotImplementedError() raise NotImplementedError()
def _on_oauth_get_user(self, access_token, callback, user): def _on_oauth_get_user(self, access_token, future, user_future):
if user_future.exception() is not None:
future.set_exception(user_future.exception())
return
user = user_future.result()
if not user: if not user:
callback(None) future.set_exception(AuthError("Error getting user"))
return return
user["access_token"] = access_token user["access_token"] = access_token
callback(user) future.set_result(user)
def _oauth_request_parameters(self, url, access_token, parameters={}, def _oauth_request_parameters(self, url, access_token, parameters={},
method="GET"): method="GET"):
@@ -395,7 +446,7 @@ class OAuthMixin(object):
args.update(parameters) args.update(parameters)
if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a": if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
signature = _oauth10a_signature(consumer_token, method, url, args, signature = _oauth10a_signature(consumer_token, method, url, args,
access_token) access_token)
else: else:
signature = _oauth_signature(consumer_token, method, url, args, signature = _oauth_signature(consumer_token, method, url, args,
access_token) access_token)
@@ -425,13 +476,13 @@ class OAuth2Mixin(object):
process. process.
""" """
args = { args = {
"redirect_uri": redirect_uri, "redirect_uri": redirect_uri,
"client_id": client_id "client_id": client_id
} }
if extra_params: if extra_params:
args.update(extra_params) args.update(extra_params)
self.redirect( self.redirect(
url_concat(self._OAUTH_AUTHORIZE_URL, args)) url_concat(self._OAUTH_AUTHORIZE_URL, args))
def _oauth_request_token_url(self, redirect_uri=None, client_id=None, def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
client_secret=None, code=None, client_secret=None, code=None,
@@ -442,7 +493,7 @@ class OAuth2Mixin(object):
code=code, code=code,
client_id=client_id, client_id=client_id,
client_secret=client_secret, client_secret=client_secret,
) )
if extra_params: if extra_params:
args.update(extra_params) args.update(extra_params)
return url_concat(url, args) return url_concat(url, args)
@@ -499,8 +550,9 @@ class TwitterMixin(OAuthMixin):
http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback( http.fetch(self._oauth_request_token_url(callback_uri=callback_uri), self.async_callback(
self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None)) self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
def twitter_request(self, path, callback, access_token=None, @_auth_return_future
post_args=None, **args): def twitter_request(self, path, callback=None, access_token=None,
post_args=None, **args):
"""Fetches the given API path, e.g., "/statuses/user_timeline/btaylor" """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
The path should not include the format (we automatically append The path should not include the format (we automatically append
@@ -553,22 +605,22 @@ class TwitterMixin(OAuthMixin):
url, access_token, all_args, method=method) url, access_token, all_args, method=method)
args.update(oauth) args.update(oauth)
if args: if args:
url += "?" + urllib.urlencode(args) url += "?" + urllib_parse.urlencode(args)
callback = self.async_callback(self._on_twitter_request, callback)
http = self.get_auth_http_client() http = self.get_auth_http_client()
http_callback = self.async_callback(self._on_twitter_request, callback)
if post_args is not None: if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args), http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
callback=callback) callback=http_callback)
else: else:
http.fetch(url, callback=callback) http.fetch(url, callback=http_callback)
def _on_twitter_request(self, callback, response): def _on_twitter_request(self, future, response):
if response.error: if response.error:
gen_log.warning("Error response %s fetching %s", response.error, future.set_exception(AuthError(
response.request.url) "Error response %s fetching %s" % (response.error,
callback(None) response.request.url)))
return return
callback(escape.json_decode(response.body)) future.set_result(escape.json_decode(response.body))
def _oauth_consumer_token(self): def _oauth_consumer_token(self):
self.require_setting("twitter_consumer_key", "Twitter OAuth") self.require_setting("twitter_consumer_key", "Twitter OAuth")
@@ -577,13 +629,12 @@ class TwitterMixin(OAuthMixin):
key=self.settings["twitter_consumer_key"], key=self.settings["twitter_consumer_key"],
secret=self.settings["twitter_consumer_secret"]) secret=self.settings["twitter_consumer_secret"])
def _oauth_get_user(self, access_token, callback): @return_future
callback = self.async_callback(self._parse_user_response, callback) @gen.engine
self.twitter_request( def _oauth_get_user_future(self, access_token, callback):
"/users/show/" + escape.native_str(access_token[b("screen_name")]), user = yield self.twitter_request(
access_token=access_token, callback=callback) "/users/show/" + escape.native_str(access_token[b"screen_name"]),
access_token=access_token)
def _parse_user_response(self, callback, user):
if user: if user:
user["username"] = user["screen_name"] user["username"] = user["screen_name"]
callback(user) callback(user)
@@ -629,6 +680,7 @@ class FriendFeedMixin(OAuthMixin):
_OAUTH_NO_CALLBACKS = True _OAUTH_NO_CALLBACKS = True
_OAUTH_VERSION = "1.0" _OAUTH_VERSION = "1.0"
@_auth_return_future
def friendfeed_request(self, path, callback, access_token=None, def friendfeed_request(self, path, callback, access_token=None,
post_args=None, **args): post_args=None, **args):
"""Fetches the given relative API path, e.g., "/bret/friends" """Fetches the given relative API path, e.g., "/bret/friends"
@@ -675,22 +727,22 @@ class FriendFeedMixin(OAuthMixin):
url, access_token, all_args, method=method) url, access_token, all_args, method=method)
args.update(oauth) args.update(oauth)
if args: if args:
url += "?" + urllib.urlencode(args) url += "?" + urllib_parse.urlencode(args)
callback = self.async_callback(self._on_friendfeed_request, callback) callback = self.async_callback(self._on_friendfeed_request, callback)
http = self.get_auth_http_client() http = self.get_auth_http_client()
if post_args is not None: if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args), http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
callback=callback) callback=callback)
else: else:
http.fetch(url, callback=callback) http.fetch(url, callback=callback)
def _on_friendfeed_request(self, callback, response): def _on_friendfeed_request(self, future, response):
if response.error: if response.error:
gen_log.warning("Error response %s fetching %s", response.error, future.set_exception(AuthError(
response.request.url) "Error response %s fetching %s" % (response.error,
callback(None) response.request.url)))
return return
callback(escape.json_decode(response.body)) future.set_result(escape.json_decode(response.body))
def _oauth_consumer_token(self): def _oauth_consumer_token(self):
self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth") self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
@@ -699,12 +751,15 @@ class FriendFeedMixin(OAuthMixin):
key=self.settings["friendfeed_consumer_key"], key=self.settings["friendfeed_consumer_key"],
secret=self.settings["friendfeed_consumer_secret"]) secret=self.settings["friendfeed_consumer_secret"])
@return_future
@gen.engine
def _oauth_get_user(self, access_token, callback): def _oauth_get_user(self, access_token, callback):
callback = self.async_callback(self._parse_user_response, callback) user = yield self.friendfeed_request(
self.friendfeed_request(
"/feedinfo/" + access_token["username"], "/feedinfo/" + access_token["username"],
include="id,name,description", access_token=access_token, include="id,name,description", access_token=access_token)
callback=callback) if user:
user["username"] = user["id"]
callback(user)
def _parse_user_response(self, callback, user): def _parse_user_response(self, callback, user):
if user: if user:
@@ -755,15 +810,16 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
callback_uri = callback_uri or self.request.uri callback_uri = callback_uri or self.request.uri
args = self._openid_args(callback_uri, ax_attrs=ax_attrs, args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
oauth_scope=oauth_scope) oauth_scope=oauth_scope)
self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args)) self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
@_auth_return_future
def get_authenticated_user(self, callback): def get_authenticated_user(self, callback):
"""Fetches the authenticated user data upon redirect.""" """Fetches the authenticated user data upon redirect."""
# Look to see if we are doing combined OpenID/OAuth # Look to see if we are doing combined OpenID/OAuth
oauth_ns = "" oauth_ns = ""
for name, values in self.request.arguments.iteritems(): for name, values in self.request.arguments.items():
if name.startswith("openid.ns.") and \ if name.startswith("openid.ns.") and \
values[-1] == u"http://specs.openid.net/extensions/oauth/1.0": values[-1] == b"http://specs.openid.net/extensions/oauth/1.0":
oauth_ns = name[10:] oauth_ns = name[10:]
break break
token = self.get_argument("openid." + oauth_ns + ".request_token", "") token = self.get_argument("openid." + oauth_ns + ".request_token", "")
@@ -773,7 +829,8 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
http.fetch(self._oauth_access_token_url(token), http.fetch(self._oauth_access_token_url(token),
self.async_callback(self._on_access_token, callback)) self.async_callback(self._on_access_token, callback))
else: else:
OpenIdMixin.get_authenticated_user(self, callback) chain_future(OpenIdMixin.get_authenticated_user(self),
callback)
def _oauth_consumer_token(self): def _oauth_consumer_token(self):
self.require_setting("google_consumer_key", "Google OAuth") self.require_setting("google_consumer_key", "Google OAuth")
@@ -782,15 +839,16 @@ class GoogleMixin(OpenIdMixin, OAuthMixin):
key=self.settings["google_consumer_key"], key=self.settings["google_consumer_key"],
secret=self.settings["google_consumer_secret"]) secret=self.settings["google_consumer_secret"])
def _oauth_get_user(self, access_token, callback): def _oauth_get_user_future(self, access_token, callback):
OpenIdMixin.get_authenticated_user(self, callback) return OpenIdMixin.get_authenticated_user(self)
class FacebookMixin(object): class FacebookMixin(object):
"""Facebook Connect authentication. """Facebook Connect authentication.
New applications should consider using `FacebookGraphMixin` below instead *Deprecated:* New applications should use `FacebookGraphMixin`
of this class. below instead of this class. This class does not support the
Future-based interface seen on other classes in this module.
To authenticate with Facebook, register your application with To authenticate with Facebook, register your application with
Facebook at http://www.facebook.com/developers/apps.php. Then Facebook at http://www.facebook.com/developers/apps.php. Then
@@ -837,11 +895,11 @@ class FacebookMixin(object):
args["cancel_url"] = urlparse.urljoin( args["cancel_url"] = urlparse.urljoin(
self.request.full_url(), cancel_uri) self.request.full_url(), cancel_uri)
if extended_permissions: if extended_permissions:
if isinstance(extended_permissions, (unicode, bytes_type)): if isinstance(extended_permissions, (unicode_type, bytes_type)):
extended_permissions = [extended_permissions] extended_permissions = [extended_permissions]
args["req_perms"] = ",".join(extended_permissions) args["req_perms"] = ",".join(extended_permissions)
self.redirect("http://www.facebook.com/login.php?" + self.redirect("http://www.facebook.com/login.php?" +
urllib.urlencode(args)) urllib_parse.urlencode(args))
def authorize_redirect(self, extended_permissions, callback_uri=None, def authorize_redirect(self, extended_permissions, callback_uri=None,
cancel_uri=None): cancel_uri=None):
@@ -923,7 +981,7 @@ class FacebookMixin(object):
args["format"] = "json" args["format"] = "json"
args["sig"] = self._signature(args) args["sig"] = self._signature(args)
url = "http://api.facebook.com/restserver.php?" + \ url = "http://api.facebook.com/restserver.php?" + \
urllib.urlencode(args) urllib_parse.urlencode(args)
http = self.get_auth_http_client() http = self.get_auth_http_client()
http.fetch(url, callback=self.async_callback( http.fetch(url, callback=self.async_callback(
self._parse_response, callback)) self._parse_response, callback))
@@ -966,7 +1024,7 @@ class FacebookMixin(object):
def _signature(self, args): def _signature(self, args):
parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())] parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
body = "".join(parts) + self.settings["facebook_secret"] body = "".join(parts) + self.settings["facebook_secret"]
if isinstance(body, unicode): if isinstance(body, unicode_type):
body = body.encode("utf-8") body = body.encode("utf-8")
return hashlib.md5(body).hexdigest() return hashlib.md5(body).hexdigest()
@@ -986,7 +1044,7 @@ class FacebookGraphMixin(OAuth2Mixin):
_OAUTH_NO_CALLBACKS = False _OAUTH_NO_CALLBACKS = False
def get_authenticated_user(self, redirect_uri, client_id, client_secret, def get_authenticated_user(self, redirect_uri, client_id, client_secret,
code, callback, extra_fields=None): code, callback, extra_fields=None):
"""Handles the login for the Facebook user, returning a user object. """Handles the login for the Facebook user, returning a user object.
Example usage:: Example usage::
@@ -1014,10 +1072,10 @@ class FacebookGraphMixin(OAuth2Mixin):
""" """
http = self.get_auth_http_client() http = self.get_auth_http_client()
args = { args = {
"redirect_uri": redirect_uri, "redirect_uri": redirect_uri,
"code": code, "code": code,
"client_id": client_id, "client_id": client_id,
"client_secret": client_secret, "client_secret": client_secret,
} }
fields = set(['id', 'name', 'first_name', 'last_name', fields = set(['id', 'name', 'first_name', 'last_name',
@@ -1026,11 +1084,11 @@ class FacebookGraphMixin(OAuth2Mixin):
fields.update(extra_fields) fields.update(extra_fields)
http.fetch(self._oauth_request_token_url(**args), http.fetch(self._oauth_request_token_url(**args),
self.async_callback(self._on_access_token, redirect_uri, client_id, self.async_callback(self._on_access_token, redirect_uri, client_id,
client_secret, callback, fields)) client_secret, callback, fields))
def _on_access_token(self, redirect_uri, client_id, client_secret, def _on_access_token(self, redirect_uri, client_id, client_secret,
callback, fields, response): callback, fields, response):
if response.error: if response.error:
gen_log.warning('Facebook auth error: %s' % str(response)) gen_log.warning('Facebook auth error: %s' % str(response))
callback(None) callback(None)
@@ -1048,7 +1106,7 @@ class FacebookGraphMixin(OAuth2Mixin):
self._on_get_user_info, callback, session, fields), self._on_get_user_info, callback, session, fields),
access_token=session["access_token"], access_token=session["access_token"],
fields=",".join(fields) fields=",".join(fields)
) )
def _on_get_user_info(self, callback, session, fields, user): def _on_get_user_info(self, callback, session, fields, user):
if user is None: if user is None:
@@ -1063,7 +1121,7 @@ class FacebookGraphMixin(OAuth2Mixin):
callback(fieldmap) callback(fieldmap)
def facebook_request(self, path, callback, access_token=None, def facebook_request(self, path, callback, access_token=None,
post_args=None, **args): post_args=None, **args):
"""Fetches the given relative API path, e.g., "/btaylor/picture" """Fetches the given relative API path, e.g., "/btaylor/picture"
If the request is a POST, post_args should be provided. Query If the request is a POST, post_args should be provided. Query
@@ -1104,11 +1162,11 @@ class FacebookGraphMixin(OAuth2Mixin):
all_args.update(args) all_args.update(args)
if all_args: if all_args:
url += "?" + urllib.urlencode(all_args) url += "?" + urllib_parse.urlencode(all_args)
callback = self.async_callback(self._on_facebook_request, callback) callback = self.async_callback(self._on_facebook_request, callback)
http = self.get_auth_http_client() http = self.get_auth_http_client()
if post_args is not None: if post_args is not None:
http.fetch(url, method="POST", body=urllib.urlencode(post_args), http.fetch(url, method="POST", body=urllib_parse.urlencode(post_args),
callback=callback) callback=callback)
else: else:
http.fetch(url, callback=callback) http.fetch(url, callback=callback)
@@ -1148,7 +1206,7 @@ def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
key_elems = [escape.utf8(consumer_token["secret"])] key_elems = [escape.utf8(consumer_token["secret"])]
key_elems.append(escape.utf8(token["secret"] if token else "")) key_elems.append(escape.utf8(token["secret"] if token else ""))
key = b("&").join(key_elems) key = b"&".join(key_elems)
hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
return binascii.b2a_base64(hash.digest())[:-1] return binascii.b2a_base64(hash.digest())[:-1]
@@ -1170,25 +1228,25 @@ def _oauth10a_signature(consumer_token, method, url, parameters={}, token=None):
for k, v in sorted(parameters.items()))) for k, v in sorted(parameters.items())))
base_string = "&".join(_oauth_escape(e) for e in base_elems) base_string = "&".join(_oauth_escape(e) for e in base_elems)
key_elems = [escape.utf8(urllib.quote(consumer_token["secret"], safe='~'))] key_elems = [escape.utf8(urllib_parse.quote(consumer_token["secret"], safe='~'))]
key_elems.append(escape.utf8(urllib.quote(token["secret"], safe='~') if token else "")) key_elems.append(escape.utf8(urllib_parse.quote(token["secret"], safe='~') if token else ""))
key = b("&").join(key_elems) key = b"&".join(key_elems)
hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1) hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
return binascii.b2a_base64(hash.digest())[:-1] return binascii.b2a_base64(hash.digest())[:-1]
def _oauth_escape(val): def _oauth_escape(val):
if isinstance(val, unicode): if isinstance(val, unicode_type):
val = val.encode("utf-8") val = val.encode("utf-8")
return urllib.quote(val, safe="~") return urllib_parse.quote(val, safe="~")
def _oauth_parse_response(body): def _oauth_parse_response(body):
p = escape.parse_qs(body, keep_blank_values=False) p = escape.parse_qs(body, keep_blank_values=False)
token = dict(key=p[b("oauth_token")][0], secret=p[b("oauth_token_secret")][0]) token = dict(key=p[b"oauth_token"][0], secret=p[b"oauth_token_secret"][0])
# Add the extra parameters the Provider included to the token # Add the extra parameters the Provider included to the token
special = (b("oauth_token"), b("oauth_token_secret")) special = (b"oauth_token", b"oauth_token_secret")
token.update((k, p[k][0]) for k in p if k not in special) token.update((k, p[k][0]) for k in p if k not in special)
return token return token
+9 -15
View File
@@ -31,7 +31,7 @@ Additionally, modifying these variables will cause reloading to behave
incorrectly. incorrectly.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import os import os
import sys import sys
@@ -79,6 +79,7 @@ import weakref
from tornado import ioloop from tornado import ioloop
from tornado.log import gen_log from tornado.log import gen_log
from tornado import process from tornado import process
from tornado.util import exec_in
try: try:
import signal import signal
@@ -91,6 +92,7 @@ _reload_hooks = []
_reload_attempted = False _reload_attempted = False
_io_loops = weakref.WeakKeyDictionary() _io_loops = weakref.WeakKeyDictionary()
def start(io_loop=None, check_time=500): def start(io_loop=None, check_time=500):
"""Restarts the process automatically when a module is modified. """Restarts the process automatically when a module is modified.
@@ -103,7 +105,7 @@ def start(io_loop=None, check_time=500):
_io_loops[io_loop] = True _io_loops[io_loop] = True
if len(_io_loops) > 1: if len(_io_loops) > 1:
gen_log.warning("tornado.autoreload started more than once in the same process") gen_log.warning("tornado.autoreload started more than once in the same process")
add_reload_hook(functools.partial(_close_all_fds, io_loop)) add_reload_hook(functools.partial(io_loop.close, all_fds=True))
modify_times = {} modify_times = {}
callback = functools.partial(_reload_on_update, modify_times) callback = functools.partial(_reload_on_update, modify_times)
scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop) scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
@@ -141,14 +143,6 @@ def add_reload_hook(fn):
_reload_hooks.append(fn) _reload_hooks.append(fn)
def _close_all_fds(io_loop):
for fd in io_loop._handlers.keys():
try:
os.close(fd)
except Exception:
pass
def _reload_on_update(modify_times): def _reload_on_update(modify_times):
if _reload_attempted: if _reload_attempted:
# We already tried to reload and it didn't work, so don't try again. # We already tried to reload and it didn't work, so don't try again.
@@ -204,7 +198,7 @@ def _reload():
# to ensure that the new process sees the same path we did. # to ensure that the new process sees the same path we did.
path_prefix = '.' + os.pathsep path_prefix = '.' + os.pathsep
if (sys.path[0] == '' and if (sys.path[0] == '' and
not os.environ.get("PYTHONPATH", "").startswith(path_prefix)): not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
os.environ["PYTHONPATH"] = (path_prefix + os.environ["PYTHONPATH"] = (path_prefix +
os.environ.get("PYTHONPATH", "")) os.environ.get("PYTHONPATH", ""))
if sys.platform == 'win32': if sys.platform == 'win32':
@@ -263,7 +257,7 @@ def main():
script = sys.argv[1] script = sys.argv[1]
sys.argv = sys.argv[1:] sys.argv = sys.argv[1:]
else: else:
print >>sys.stderr, _USAGE print(_USAGE, file=sys.stderr)
sys.exit(1) sys.exit(1)
try: try:
@@ -277,11 +271,11 @@ def main():
# Use globals as our "locals" dictionary so that # Use globals as our "locals" dictionary so that
# something that tries to import __main__ (e.g. the unittest # something that tries to import __main__ (e.g. the unittest
# module) will see the right things. # module) will see the right things.
exec f.read() in globals(), globals() exec_in(f.read(), globals(), globals())
except SystemExit, e: except SystemExit as e:
logging.basicConfig() logging.basicConfig()
gen_log.info("Script exited with status %s", e.code) gen_log.info("Script exited with status %s", e.code)
except Exception, e: except Exception as e:
logging.basicConfig() logging.basicConfig()
gen_log.warning("Script exited with uncaught exception", exc_info=True) gen_log.warning("Script exited with uncaught exception", exc_info=True)
# If an exception occurred at import time, the file with the error # If an exception occurred at import time, the file with the error
+72 -10
View File
@@ -13,19 +13,21 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import functools import functools
import sys import sys
from tornado.stack_context import ExceptionStackContext from tornado.stack_context import ExceptionStackContext
from tornado.util import raise_exc_info from tornado.util import raise_exc_info, ArgReplacer
try: try:
from concurrent import futures from concurrent import futures
except ImportError: except ImportError:
futures = None futures = None
class ReturnValueIgnoredError(Exception):
pass
class DummyFuture(object): class DummyFuture(object):
def __init__(self): def __init__(self):
@@ -89,39 +91,99 @@ if futures is None:
else: else:
Future = futures.Future Future = futures.Future
class DummyExecutor(object): class DummyExecutor(object):
def submit(self, fn, *args, **kwargs): def submit(self, fn, *args, **kwargs):
future = Future() future = Future()
try: try:
future.set_result(fn(*args, **kwargs)) future.set_result(fn(*args, **kwargs))
except Exception, e: except Exception as e:
future.set_exception(e) future.set_exception(e)
return future return future
dummy_executor = DummyExecutor() dummy_executor = DummyExecutor()
def run_on_executor(fn): def run_on_executor(fn):
@functools.wraps(fn) @functools.wraps(fn)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
callback = kwargs.pop("callback") callback = kwargs.pop("callback", None)
future = self.executor.submit(fn, self, *args, **kwargs) future = self.executor.submit(fn, self, *args, **kwargs)
if callback: if callback:
self.io_loop.add_future(future, callback) self.io_loop.add_future(future, callback)
return future return future
return wrapper return wrapper
# TODO: this needs a better name
def future_wrap(f): def return_future(f):
"""Decorator to make a function that returns via callback return a `Future`.
The wrapped function should take a ``callback`` keyword argument
and invoke it with one argument when it has finished. To signal failure,
the function can simply raise an exception (which will be
captured by the `stack_context` and passed along to the `Future`).
From the caller's perspective, the callback argument is optional.
If one is given, it will be invoked when the function is complete
with the `Future` as an argument. If no callback is given, the caller
should use the `Future` to wait for the function to complete
(perhaps by yielding it in a `gen.engine` function, or passing it
to `IOLoop.add_future`).
Usage::
@return_future
def future_func(arg1, arg2, callback):
# Do stuff (possibly asynchronous)
callback(result)
@gen.engine
def caller(callback):
yield future_func(arg1, arg2)
callback()
Note that ``@return_future`` and ``@gen.engine`` can be applied to the
same function, provided ``@return_future`` appears first.
"""
replacer = ArgReplacer(f, 'callback')
@functools.wraps(f) @functools.wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
future = Future() future = Future()
if kwargs.get('callback') is not None: callback, args, kwargs = replacer.replace(future.set_result,
future.add_done_callback(kwargs.pop('callback')) args, kwargs)
kwargs['callback'] = future.set_result if callback is not None:
future.add_done_callback(callback)
def handle_error(typ, value, tb): def handle_error(typ, value, tb):
future.set_exception(value) future.set_exception(value)
return True return True
exc_info = None
with ExceptionStackContext(handle_error): with ExceptionStackContext(handle_error):
f(*args, **kwargs) try:
result = f(*args, **kwargs)
if result is not None:
raise ReturnValueIgnoredError(
"@return_future should not be used with functions "
"that return values")
except:
exc_info = sys.exc_info()
raise
if exc_info is not None:
# If the initial synchronous part of f() raised an exception,
# go ahead and raise it to the caller directly without waiting
# for them to inspect the Future.
raise_exc_info(exc_info)
return future return future
return wrapper return wrapper
def chain_future(a, b):
"""Chain two futures together so that when one completes, so does the other.
The result (success or failure) of ``a`` will be copied to ``b``.
"""
def copy(future):
assert future is a
if a.exception() is not None:
b.set_exception(a.exception())
else:
b.set_result(a.result())
a.add_done_callback(copy)
+48 -28
View File
@@ -16,9 +16,8 @@
"""Blocking and non-blocking HTTP client implementations using pycurl.""" """Blocking and non-blocking HTTP client implementations using pycurl."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import cStringIO
import collections import collections
import logging import logging
import pycurl import pycurl
@@ -30,20 +29,22 @@ from tornado import ioloop
from tornado.log import gen_log from tornado.log import gen_log
from tornado import stack_context from tornado import stack_context
from tornado.escape import utf8 from tornado.escape import utf8, native_str
from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy
try:
from io import BytesIO # py3
except ImportError:
from cStringIO import StringIO as BytesIO # py2
class CurlAsyncHTTPClient(AsyncHTTPClient): class CurlAsyncHTTPClient(AsyncHTTPClient):
def initialize(self, io_loop=None, max_clients=10, defaults=None): def initialize(self, io_loop, max_clients=10, defaults=None):
self.io_loop = io_loop super(CurlAsyncHTTPClient, self).initialize(io_loop, defaults=defaults)
self.defaults = dict(HTTPRequest._DEFAULTS)
if defaults is not None:
self.defaults.update(defaults)
self._multi = pycurl.CurlMulti() self._multi = pycurl.CurlMulti()
self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout) self._multi.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout)
self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket) self._multi.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket)
self._curls = [_curl_create() for i in xrange(max_clients)] self._curls = [_curl_create() for i in range(max_clients)]
self._free_list = self._curls[:] self._free_list = self._curls[:]
self._requests = collections.deque() self._requests = collections.deque()
self._fds = {} self._fds = {}
@@ -69,19 +70,27 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
self._handle_force_timeout, 1000, io_loop=io_loop) self._handle_force_timeout, 1000, io_loop=io_loop)
self._force_timeout_callback.start() self._force_timeout_callback.start()
# Work around a bug in libcurl 7.29.0: Some fields in the curl
# multi object are initialized lazily, and its destructor will
# segfault if it is destroyed without having been used. Add
# and remove a dummy handle to make sure everything is
# initialized.
dummy_curl_handle = pycurl.Curl()
self._multi.add_handle(dummy_curl_handle)
self._multi.remove_handle(dummy_curl_handle)
def close(self): def close(self):
self._force_timeout_callback.stop() self._force_timeout_callback.stop()
if self._timeout is not None:
self.io_loop.remove_timeout(self._timeout)
for curl in self._curls: for curl in self._curls:
curl.close() curl.close()
self._multi.close() self._multi.close()
self._closed = True self._closed = True
super(CurlAsyncHTTPClient, self).close() super(CurlAsyncHTTPClient, self).close()
def fetch(self, request, callback, **kwargs): def fetch_impl(self, request, callback):
if not isinstance(request, HTTPRequest): self._requests.append((request, callback))
request = HTTPRequest(url=request, **kwargs)
request = _RequestProxy(request, self.defaults)
self._requests.append((request, stack_context.wrap(callback)))
self._process_queue() self._process_queue()
self._set_timeout(0) self._set_timeout(0)
@@ -128,7 +137,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
while True: while True:
try: try:
ret, num_handles = self._socket_action(fd, action) ret, num_handles = self._socket_action(fd, action)
except pycurl.error, e: except pycurl.error as e:
ret = e.args[0] ret = e.args[0]
if ret != pycurl.E_CALL_MULTI_PERFORM: if ret != pycurl.E_CALL_MULTI_PERFORM:
break break
@@ -142,7 +151,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
try: try:
ret, num_handles = self._socket_action( ret, num_handles = self._socket_action(
pycurl.SOCKET_TIMEOUT, 0) pycurl.SOCKET_TIMEOUT, 0)
except pycurl.error, e: except pycurl.error as e:
ret = e.args[0] ret = e.args[0]
if ret != pycurl.E_CALL_MULTI_PERFORM: if ret != pycurl.E_CALL_MULTI_PERFORM:
break break
@@ -173,7 +182,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
while True: while True:
try: try:
ret, num_handles = self._multi.socket_all() ret, num_handles = self._multi.socket_all()
except pycurl.error, e: except pycurl.error as e:
ret = e.args[0] ret = e.args[0]
if ret != pycurl.E_CALL_MULTI_PERFORM: if ret != pycurl.E_CALL_MULTI_PERFORM:
break break
@@ -203,7 +212,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
(request, callback) = self._requests.popleft() (request, callback) = self._requests.popleft()
curl.info = { curl.info = {
"headers": httputil.HTTPHeaders(), "headers": httputil.HTTPHeaders(),
"buffer": cStringIO.StringIO(), "buffer": BytesIO(),
"request": request, "request": request,
"callback": callback, "callback": callback,
"curl_start_time": time.time(), "curl_start_time": time.time(),
@@ -247,7 +256,7 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME), starttransfer=curl.getinfo(pycurl.STARTTRANSFER_TIME),
total=curl.getinfo(pycurl.TOTAL_TIME), total=curl.getinfo(pycurl.TOTAL_TIME),
redirect=curl.getinfo(pycurl.REDIRECT_TIME), redirect=curl.getinfo(pycurl.REDIRECT_TIME),
) )
try: try:
info["callback"](HTTPResponse( info["callback"](HTTPResponse(
request=info["request"], code=code, headers=info["headers"], request=info["request"], code=code, headers=info["headers"],
@@ -276,7 +285,7 @@ def _curl_create():
def _curl_setup_request(curl, request, buffer, headers): def _curl_setup_request(curl, request, buffer, headers):
curl.setopt(pycurl.URL, utf8(request.url)) curl.setopt(pycurl.URL, native_str(request.url))
# libcurl's magic "Expect: 100-continue" behavior causes delays # libcurl's magic "Expect: 100-continue" behavior causes delays
# with servers that don't support it (which include, among others, # with servers that don't support it (which include, among others,
@@ -296,10 +305,10 @@ def _curl_setup_request(curl, request, buffer, headers):
# Request headers may be either a regular dict or HTTPHeaders object # Request headers may be either a regular dict or HTTPHeaders object
if isinstance(request.headers, httputil.HTTPHeaders): if isinstance(request.headers, httputil.HTTPHeaders):
curl.setopt(pycurl.HTTPHEADER, curl.setopt(pycurl.HTTPHEADER,
[utf8("%s: %s" % i) for i in request.headers.get_all()]) [native_str("%s: %s" % i) for i in request.headers.get_all()])
else: else:
curl.setopt(pycurl.HTTPHEADER, curl.setopt(pycurl.HTTPHEADER,
[utf8("%s: %s" % i) for i in request.headers.iteritems()]) [native_str("%s: %s" % i) for i in request.headers.items()])
if request.header_callback: if request.header_callback:
curl.setopt(pycurl.HEADERFUNCTION, request.header_callback) curl.setopt(pycurl.HEADERFUNCTION, request.header_callback)
@@ -307,15 +316,26 @@ def _curl_setup_request(curl, request, buffer, headers):
curl.setopt(pycurl.HEADERFUNCTION, curl.setopt(pycurl.HEADERFUNCTION,
lambda line: _curl_header_callback(headers, line)) lambda line: _curl_header_callback(headers, line))
if request.streaming_callback: if request.streaming_callback:
curl.setopt(pycurl.WRITEFUNCTION, request.streaming_callback) write_function = request.streaming_callback
else: else:
curl.setopt(pycurl.WRITEFUNCTION, buffer.write) write_function = buffer.write
if type(b'') is type(''): # py2
curl.setopt(pycurl.WRITEFUNCTION, write_function)
else: # py3
# Upstream pycurl doesn't support py3, but ubuntu 12.10 includes
# a fork/port. That version has a bug in which it passes unicode
# strings instead of bytes to the WRITEFUNCTION. This means that
# if you use a WRITEFUNCTION (which tornado always does), you cannot
# download arbitrary binary data. This needs to be fixed in the
# ported pycurl package, but in the meantime this lambda will
# make it work for downloading (utf8) text.
curl.setopt(pycurl.WRITEFUNCTION, lambda s: write_function(utf8(s)))
curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects) curl.setopt(pycurl.FOLLOWLOCATION, request.follow_redirects)
curl.setopt(pycurl.MAXREDIRS, request.max_redirects) curl.setopt(pycurl.MAXREDIRS, request.max_redirects)
curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout)) curl.setopt(pycurl.CONNECTTIMEOUT_MS, int(1000 * request.connect_timeout))
curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout)) curl.setopt(pycurl.TIMEOUT_MS, int(1000 * request.request_timeout))
if request.user_agent: if request.user_agent:
curl.setopt(pycurl.USERAGENT, utf8(request.user_agent)) curl.setopt(pycurl.USERAGENT, native_str(request.user_agent))
else: else:
curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)") curl.setopt(pycurl.USERAGENT, "Mozilla/5.0 (compatible; pycurl)")
if request.network_interface: if request.network_interface:
@@ -329,7 +349,7 @@ def _curl_setup_request(curl, request, buffer, headers):
curl.setopt(pycurl.PROXYPORT, request.proxy_port) curl.setopt(pycurl.PROXYPORT, request.proxy_port)
if request.proxy_username: if request.proxy_username:
credentials = '%s:%s' % (request.proxy_username, credentials = '%s:%s' % (request.proxy_username,
request.proxy_password) request.proxy_password)
curl.setopt(pycurl.PROXYUSERPWD, credentials) curl.setopt(pycurl.PROXYUSERPWD, credentials)
else: else:
curl.setopt(pycurl.PROXY, '') curl.setopt(pycurl.PROXY, '')
@@ -377,7 +397,7 @@ def _curl_setup_request(curl, request, buffer, headers):
# Handle curl's cryptic options for every individual HTTP method # Handle curl's cryptic options for every individual HTTP method
if request.method in ("POST", "PUT"): if request.method in ("POST", "PUT"):
request_buffer = cStringIO.StringIO(utf8(request.body)) request_buffer = BytesIO(utf8(request.body))
curl.setopt(pycurl.READFUNCTION, request_buffer.read) curl.setopt(pycurl.READFUNCTION, request_buffer.read)
if request.method == "POST": if request.method == "POST":
def ioctl(cmd): def ioctl(cmd):
@@ -391,7 +411,7 @@ def _curl_setup_request(curl, request, buffer, headers):
if request.auth_username is not None: if request.auth_username is not None:
userpwd = "%s:%s" % (request.auth_username, request.auth_password or '') userpwd = "%s:%s" % (request.auth_username, request.auth_password or '')
curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC)
curl.setopt(pycurl.USERPWD, utf8(userpwd)) curl.setopt(pycurl.USERPWD, native_str(userpwd))
gen_log.debug("%s %s (username: %r)", request.method, request.url, gen_log.debug("%s %s (username: %r)", request.method, request.url,
request.auth_username) request.auth_username)
else: else:
-112
View File
@@ -1,112 +0,0 @@
/*
* Copyright 2009 Facebook
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#include "Python.h"
#include <string.h>
#include <sys/epoll.h>
#define MAX_EVENTS 24
/*
* Simple wrapper around epoll_create.
*/
static PyObject* _epoll_create(void) {
int fd = epoll_create(MAX_EVENTS);
if (fd == -1) {
PyErr_SetFromErrno(PyExc_Exception);
return NULL;
}
return PyInt_FromLong(fd);
}
/*
* Simple wrapper around epoll_ctl. We throw an exception if the call fails
* rather than returning the error code since it is an infrequent (and likely
* catastrophic) event when it does happen.
*/
static PyObject* _epoll_ctl(PyObject* self, PyObject* args) {
int epfd, op, fd, events;
struct epoll_event event;
if (!PyArg_ParseTuple(args, "iiiI", &epfd, &op, &fd, &events)) {
return NULL;
}
memset(&event, 0, sizeof(event));
event.events = events;
event.data.fd = fd;
if (epoll_ctl(epfd, op, fd, &event) == -1) {
PyErr_SetFromErrno(PyExc_OSError);
return NULL;
}
Py_INCREF(Py_None);
return Py_None;
}
/*
* Simple wrapper around epoll_wait. We return None if the call times out and
* throw an exception if an error occurs. Otherwise, we return a list of
* (fd, event) tuples.
*/
static PyObject* _epoll_wait(PyObject* self, PyObject* args) {
struct epoll_event events[MAX_EVENTS];
int epfd, timeout, num_events, i;
PyObject* list;
PyObject* tuple;
if (!PyArg_ParseTuple(args, "ii", &epfd, &timeout)) {
return NULL;
}
Py_BEGIN_ALLOW_THREADS
num_events = epoll_wait(epfd, events, MAX_EVENTS, timeout);
Py_END_ALLOW_THREADS
if (num_events == -1) {
PyErr_SetFromErrno(PyExc_Exception);
return NULL;
}
list = PyList_New(num_events);
for (i = 0; i < num_events; i++) {
tuple = PyTuple_New(2);
PyTuple_SET_ITEM(tuple, 0, PyInt_FromLong(events[i].data.fd));
PyTuple_SET_ITEM(tuple, 1, PyInt_FromLong(events[i].events));
PyList_SET_ITEM(list, i, tuple);
}
return list;
}
/*
* Our method declararations
*/
static PyMethodDef kEpollMethods[] = {
{"epoll_create", (PyCFunction)_epoll_create, METH_NOARGS,
"Create an epoll file descriptor"},
{"epoll_ctl", _epoll_ctl, METH_VARARGS,
"Control an epoll file descriptor"},
{"epoll_wait", _epoll_wait, METH_VARARGS,
"Wait for events on an epoll file descriptor"},
{NULL, NULL, 0, NULL}
};
/*
* Module initialization
*/
PyMODINIT_FUNC initepoll(void) {
Py_InitModule("epoll", kEpollMethods);
}
+41 -54
View File
@@ -20,49 +20,34 @@ Also includes a few other miscellaneous string manipulation functions that
have crept in over time. have crept in over time.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import htmlentitydefs
import re import re
import sys import sys
import urllib
# Python3 compatibility: On python2.5, introduce the bytes alias from 2.6 from tornado.util import bytes_type, unicode_type, basestring_type, u
try:
bytes
except Exception:
bytes = str
try: try:
from urlparse import parse_qs # Python 2.6+ from urllib.parse import parse_qs # py3
except ImportError: except ImportError:
from cgi import parse_qs from urlparse import parse_qs # Python 2.6+
# json module is in the standard library as of python 2.6; fall back to
# simplejson if present for older versions.
try: try:
import json import htmlentitydefs # py2
assert hasattr(json, "loads") and hasattr(json, "dumps") except ImportError:
_json_decode = json.loads import html.entities as htmlentitydefs # py3
_json_encode = json.dumps
except Exception:
try:
import simplejson
_json_decode = lambda s: simplejson.loads(_unicode(s))
_json_encode = lambda v: simplejson.dumps(v)
except ImportError:
try:
# For Google AppEngine
from django.utils import simplejson
_json_decode = lambda s: simplejson.loads(_unicode(s))
_json_encode = lambda v: simplejson.dumps(v)
except ImportError:
def _json_decode(s):
raise NotImplementedError(
"A JSON parser is required, e.g., simplejson at "
"http://pypi.python.org/pypi/simplejson/")
_json_encode = _json_decode
try:
import urllib.parse as urllib_parse # py3
except ImportError:
import urllib as urllib_parse # py2
import json
try:
unichr
except NameError:
unichr = chr
_XHTML_ESCAPE_RE = re.compile('[&<>"]') _XHTML_ESCAPE_RE = re.compile('[&<>"]')
_XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'} _XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}
@@ -87,12 +72,12 @@ def json_encode(value):
# the javscript. Some json libraries do this escaping by default, # the javscript. Some json libraries do this escaping by default,
# although python's standard library does not, so we do it here. # although python's standard library does not, so we do it here.
# http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped
return _json_encode(recursive_unicode(value)).replace("</", "<\\/") return json.dumps(recursive_unicode(value)).replace("</", "<\\/")
def json_decode(value): def json_decode(value):
"""Returns Python objects for the given JSON string.""" """Returns Python objects for the given JSON string."""
return _json_decode(to_basestring(value)) return json.loads(to_basestring(value))
def squeeze(value): def squeeze(value):
@@ -102,7 +87,7 @@ def squeeze(value):
def url_escape(value): def url_escape(value):
"""Returns a valid URL-encoded version of the given value.""" """Returns a valid URL-encoded version of the given value."""
return urllib.quote_plus(utf8(value)) return urllib_parse.quote_plus(utf8(value))
# python 3 changed things around enough that we need two separate # python 3 changed things around enough that we need two separate
# implementations of url_unescape. We also need our own implementation # implementations of url_unescape. We also need our own implementation
@@ -117,9 +102,9 @@ if sys.version_info[0] < 3:
the result is a unicode string in the specified encoding. the result is a unicode string in the specified encoding.
""" """
if encoding is None: if encoding is None:
return urllib.unquote_plus(utf8(value)) return urllib_parse.unquote_plus(utf8(value))
else: else:
return unicode(urllib.unquote_plus(utf8(value)), encoding) return unicode_type(urllib_parse.unquote_plus(utf8(value)), encoding)
parse_qs_bytes = parse_qs parse_qs_bytes = parse_qs
else: else:
@@ -132,9 +117,9 @@ else:
the result is a unicode string in the specified encoding. the result is a unicode string in the specified encoding.
""" """
if encoding is None: if encoding is None:
return urllib.parse.unquote_to_bytes(value) return urllib_parse.unquote_to_bytes(value)
else: else:
return urllib.unquote_plus(to_basestring(value), encoding=encoding) return urllib_parse.unquote_plus(to_basestring(value), encoding=encoding)
def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False): def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False):
"""Parses a query string like urlparse.parse_qs, but returns the """Parses a query string like urlparse.parse_qs, but returns the
@@ -149,12 +134,12 @@ else:
result = parse_qs(qs, keep_blank_values, strict_parsing, result = parse_qs(qs, keep_blank_values, strict_parsing,
encoding='latin1', errors='strict') encoding='latin1', errors='strict')
encoded = {} encoded = {}
for k, v in result.iteritems(): for k, v in result.items():
encoded[k] = [i.encode('latin1') for i in v] encoded[k] = [i.encode('latin1') for i in v]
return encoded return encoded
_UTF8_TYPES = (bytes, type(None)) _UTF8_TYPES = (bytes_type, type(None))
def utf8(value): def utf8(value):
@@ -165,10 +150,10 @@ def utf8(value):
""" """
if isinstance(value, _UTF8_TYPES): if isinstance(value, _UTF8_TYPES):
return value return value
assert isinstance(value, unicode) assert isinstance(value, unicode_type)
return value.encode("utf-8") return value.encode("utf-8")
_TO_UNICODE_TYPES = (unicode, type(None)) _TO_UNICODE_TYPES = (unicode_type, type(None))
def to_unicode(value): def to_unicode(value):
@@ -179,7 +164,7 @@ def to_unicode(value):
""" """
if isinstance(value, _TO_UNICODE_TYPES): if isinstance(value, _TO_UNICODE_TYPES):
return value return value
assert isinstance(value, bytes) assert isinstance(value, bytes_type)
return value.decode("utf-8") return value.decode("utf-8")
# to_unicode was previously named _unicode not because it was private, # to_unicode was previously named _unicode not because it was private,
@@ -188,12 +173,12 @@ _unicode = to_unicode
# When dealing with the standard library across python 2 and 3 it is # When dealing with the standard library across python 2 and 3 it is
# sometimes useful to have a direct conversion to the native string type # sometimes useful to have a direct conversion to the native string type
if str is unicode: if str is unicode_type:
native_str = to_unicode native_str = to_unicode
else: else:
native_str = utf8 native_str = utf8
_BASESTRING_TYPES = (basestring, type(None)) _BASESTRING_TYPES = (basestring_type, type(None))
def to_basestring(value): def to_basestring(value):
@@ -207,7 +192,7 @@ def to_basestring(value):
""" """
if isinstance(value, _BASESTRING_TYPES): if isinstance(value, _BASESTRING_TYPES):
return value return value
assert isinstance(value, bytes) assert isinstance(value, bytes_type)
return value.decode("utf-8") return value.decode("utf-8")
@@ -217,12 +202,12 @@ def recursive_unicode(obj):
Supports lists, tuples, and dictionaries. Supports lists, tuples, and dictionaries.
""" """
if isinstance(obj, dict): if isinstance(obj, dict):
return dict((recursive_unicode(k), recursive_unicode(v)) for (k, v) in obj.iteritems()) return dict((recursive_unicode(k), recursive_unicode(v)) for (k, v) in obj.items())
elif isinstance(obj, list): elif isinstance(obj, list):
return list(recursive_unicode(i) for i in obj) return list(recursive_unicode(i) for i in obj)
elif isinstance(obj, tuple): elif isinstance(obj, tuple):
return tuple(recursive_unicode(i) for i in obj) return tuple(recursive_unicode(i) for i in obj)
elif isinstance(obj, bytes): elif isinstance(obj, bytes_type):
return to_unicode(obj) return to_unicode(obj)
else: else:
return obj return obj
@@ -232,7 +217,9 @@ def recursive_unicode(obj):
# but it gets all exponential on certain patterns (such as too many trailing # but it gets all exponential on certain patterns (such as too many trailing
# dots), causing the regex matcher to never return. # dots), causing the regex matcher to never return.
# This regex should avoid those problems. # This regex should avoid those problems.
_URL_RE = re.compile(ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)""") # Use to_unicode instead of tornado.util.u - we don't want backslashes getting
# processed as escapes.
_URL_RE = re.compile(to_unicode(r"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)"""))
def linkify(text, shorten=False, extra_params="", def linkify(text, shorten=False, extra_params="",
@@ -302,7 +289,7 @@ def linkify(text, shorten=False, extra_params="",
# (no more slug, etc), so it really just provides a little # (no more slug, etc), so it really just provides a little
# extra indication of shortening. # extra indication of shortening.
url = url[:proto_len] + parts[0] + "/" + \ url = url[:proto_len] + parts[0] + "/" + \
parts[1][:8].split('?')[0].split('.')[0] parts[1][:8].split('?')[0].split('.')[0]
if len(url) > max_len * 1.5: # still too long if len(url) > max_len * 1.5: # still too long
url = url[:max_len] url = url[:max_len]
@@ -321,7 +308,7 @@ def linkify(text, shorten=False, extra_params="",
# have a status bar, such as Safari by default) # have a status bar, such as Safari by default)
params += ' title="%s"' % href params += ' title="%s"' % href
return u'<a href="%s"%s>%s</a>' % (href, params, url) return u('<a href="%s"%s>%s</a>') % (href, params, url)
# First HTML-escape so that our strings are all safe. # First HTML-escape so that our strings are all safe.
# The regex is modified to avoid character entites other than &amp; so # The regex is modified to avoid character entites other than &amp; so
@@ -344,7 +331,7 @@ def _convert_entity(m):
def _build_unicode_map(): def _build_unicode_map():
unicode_map = {} unicode_map = {}
for name, value in htmlentitydefs.name2codepoint.iteritems(): for name, value in htmlentitydefs.name2codepoint.items():
unicode_map[name] = unichr(value) unicode_map[name] = unichr(value)
return unicode_map return unicode_map
+22 -28
View File
@@ -62,10 +62,11 @@ it was called with one argument, the result is that argument. If it was
called with more than one argument or any keyword arguments, the result called with more than one argument or any keyword arguments, the result
is an `Arguments` object, which is a named tuple ``(args, kwargs)``. is an `Arguments` object, which is a named tuple ``(args, kwargs)``.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import collections
import functools import functools
import operator import itertools
import sys import sys
import types import types
@@ -276,15 +277,23 @@ class Multi(YieldPoint):
a list of ``YieldPoints``. a list of ``YieldPoints``.
""" """
def __init__(self, children): def __init__(self, children):
assert all(isinstance(i, YieldPoint) for i in children) self.children = []
self.children = children for i in children:
if isinstance(i, Future):
i = YieldFuture(i)
self.children.append(i)
assert all(isinstance(i, YieldPoint) for i in self.children)
self.unfinished_children = set(self.children)
def start(self, runner): def start(self, runner):
for i in self.children: for i in self.children:
i.start(runner) i.start(runner)
def is_ready(self): def is_ready(self):
return all(i.is_ready() for i in self.children) finished = list(itertools.takewhile(
lambda i: i.is_ready(), self.unfinished_children))
self.unfinished_children.difference_update(finished)
return not self.unfinished_children
def get_result(self): def get_result(self):
return [i.get_result() for i in self.children] return [i.get_result() for i in self.children]
@@ -305,10 +314,12 @@ class Runner(object):
"""Internal implementation of `tornado.gen.engine`. """Internal implementation of `tornado.gen.engine`.
Maintains information about pending callbacks and their results. Maintains information about pending callbacks and their results.
``final_callback`` is run after the generator exits.
""" """
def __init__(self, gen, deactivate_stack_context): def __init__(self, gen, final_callback):
self.gen = gen self.gen = gen
self.deactivate_stack_context = deactivate_stack_context self.final_callback = final_callback
self.yield_point = _NullYieldPoint() self.yield_point = _NullYieldPoint()
self.pending_callbacks = set() self.pending_callbacks = set()
self.results = {} self.results = {}
@@ -373,16 +384,15 @@ class Runner(object):
raise LeakedCallbackError( raise LeakedCallbackError(
"finished without waiting for callbacks %r" % "finished without waiting for callbacks %r" %
self.pending_callbacks) self.pending_callbacks)
self.deactivate_stack_context() self.final_callback()
self.deactivate_stack_context = None self.final_callback = None
return return
except Exception: except Exception:
self.finished = True self.finished = True
raise raise
if isinstance(yielded, list): if isinstance(yielded, list):
yielded = Multi(yielded) yielded = Multi(yielded)
if isinstance(yielded, Future): elif isinstance(yielded, Future):
# TODO: lists of futures
yielded = YieldFuture(yielded) yielded = YieldFuture(yielded)
if isinstance(yielded, YieldPoint): if isinstance(yielded, YieldPoint):
self.yield_point = yielded self.yield_point = yielded
@@ -414,20 +424,4 @@ class Runner(object):
else: else:
return False return False
# in python 2.6+ this could be a collections.namedtuple Arguments = collections.namedtuple('Arguments', ['args', 'kwargs'])
class Arguments(tuple):
"""The result of a yield expression whose callback had more than one
argument (or keyword arguments).
The `Arguments` object can be used as a tuple ``(args, kwargs)``
or an object with attributes ``args`` and ``kwargs``.
"""
__slots__ = ()
def __new__(cls, args, kwargs):
return tuple.__new__(cls, (args, kwargs))
args = property(operator.itemgetter(0))
kwargs = property(operator.itemgetter(1))
+50 -15
View File
@@ -29,14 +29,12 @@ you use a recent version of ``libcurl`` and ``pycurl``. Currently the minimum
supported version is 7.18.2, and the recommended version is 7.21.1 or newer. supported version is 7.18.2, and the recommended version is 7.21.1 or newer.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import calendar
import email.utils
import httplib
import time import time
import weakref import weakref
from tornado.concurrent import Future
from tornado.escape import utf8 from tornado.escape import utf8
from tornado import httputil, stack_context from tornado import httputil, stack_context
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
@@ -134,7 +132,7 @@ class AsyncHTTPClient(Configurable):
def _async_clients(cls): def _async_clients(cls):
attr_name = '_async_client_dict_' + cls.__name__ attr_name = '_async_client_dict_' + cls.__name__
if not hasattr(cls, attr_name): if not hasattr(cls, attr_name):
setattr(cls, attr_name, weakref.WeakKeyDictionary()) setattr(cls, attr_name, weakref.WeakKeyDictionary())
return getattr(cls, attr_name) return getattr(cls, attr_name)
def __new__(cls, io_loop=None, force_instance=False, **kwargs): def __new__(cls, io_loop=None, force_instance=False, **kwargs):
@@ -147,6 +145,12 @@ class AsyncHTTPClient(Configurable):
cls._async_clients()[io_loop] = instance cls._async_clients()[io_loop] = instance
return instance return instance
def initialize(self, io_loop, defaults=None):
self.io_loop = io_loop
self.defaults = dict(HTTPRequest._DEFAULTS)
if defaults is not None:
self.defaults.update(defaults)
def close(self): def close(self):
"""Destroys this http client, freeing any file descriptors used. """Destroys this http client, freeing any file descriptors used.
Not needed in normal use, but may be helpful in unittests that Not needed in normal use, but may be helpful in unittests that
@@ -156,7 +160,7 @@ class AsyncHTTPClient(Configurable):
if self._async_clients().get(self.io_loop) is self: if self._async_clients().get(self.io_loop) is self:
del self._async_clients()[self.io_loop] del self._async_clients()[self.io_loop]
def fetch(self, request, callback, **kwargs): def fetch(self, request, callback=None, **kwargs):
"""Executes a request, calling callback with an `HTTPResponse`. """Executes a request, calling callback with an `HTTPResponse`.
The request may be either a string URL or an `HTTPRequest` object. The request may be either a string URL or an `HTTPRequest` object.
@@ -168,6 +172,37 @@ class AsyncHTTPClient(Configurable):
encountered during the request. You can call response.rethrow() to encountered during the request. You can call response.rethrow() to
throw the exception (if any) in the callback. throw the exception (if any) in the callback.
""" """
if not isinstance(request, HTTPRequest):
request = HTTPRequest(url=request, **kwargs)
# We may modify this (to add Host, Accept-Encoding, etc),
# so make sure we don't modify the caller's object. This is also
# where normal dicts get converted to HTTPHeaders objects.
request.headers = httputil.HTTPHeaders(request.headers)
request = _RequestProxy(request, self.defaults)
future = Future()
if callback is not None:
callback = stack_context.wrap(callback)
def handle_future(future):
exc = future.exception()
if isinstance(exc, HTTPError) and exc.response is not None:
response = exc.response
elif exc is not None:
response = HTTPResponse(
request, 599, error=exc,
request_time=time.time() - request.start_time)
else:
response = future.result()
self.io_loop.add_callback(callback, response)
future.add_done_callback(handle_future)
def handle_response(response):
if response.error:
future.set_exception(response.error)
else:
future.set_result(response)
self.fetch_impl(request, handle_response)
return future
def fetch_impl(self, request, callback):
raise NotImplementedError() raise NotImplementedError()
@classmethod @classmethod
@@ -280,9 +315,8 @@ class HTTPRequest(object):
if headers is None: if headers is None:
headers = httputil.HTTPHeaders() headers = httputil.HTTPHeaders()
if if_modified_since: if if_modified_since:
timestamp = calendar.timegm(if_modified_since.utctimetuple()) headers["If-Modified-Since"] = httputil.format_timestamp(
headers["If-Modified-Since"] = email.utils.formatdate( if_modified_since)
timestamp, localtime=False, usegmt=True)
self.proxy_host = proxy_host self.proxy_host = proxy_host
self.proxy_port = proxy_port self.proxy_port = proxy_port
self.proxy_username = proxy_username self.proxy_username = proxy_username
@@ -346,7 +380,7 @@ class HTTPResponse(object):
time_info=None, reason=None): time_info=None, reason=None):
self.request = request self.request = request
self.code = code self.code = code
self.reason = reason or httplib.responses.get(code, "Unknown") self.reason = reason or httputil.responses.get(code, "Unknown")
if headers is not None: if headers is not None:
self.headers = headers self.headers = headers
else: else:
@@ -383,7 +417,7 @@ class HTTPResponse(object):
raise self.error raise self.error
def __repr__(self): def __repr__(self):
args = ",".join("%s=%r" % i for i in self.__dict__.iteritems()) args = ",".join("%s=%r" % i for i in sorted(self.__dict__.items()))
return "%s(%s)" % (self.__class__.__name__, args) return "%s(%s)" % (self.__class__.__name__, args)
@@ -403,10 +437,11 @@ class HTTPError(Exception):
""" """
def __init__(self, code, message=None, response=None): def __init__(self, code, message=None, response=None):
self.code = code self.code = code
message = message or httplib.responses.get(code, "Unknown") message = message or httputil.responses.get(code, "Unknown")
self.response = response self.response = response
Exception.__init__(self, "HTTP %d: %s" % (self.code, message)) Exception.__init__(self, "HTTP %d: %s" % (self.code, message))
class _RequestProxy(object): class _RequestProxy(object):
"""Combines an object with a dictionary of defaults. """Combines an object with a dictionary of defaults.
@@ -440,15 +475,15 @@ def main():
follow_redirects=options.follow_redirects, follow_redirects=options.follow_redirects,
validate_cert=options.validate_cert, validate_cert=options.validate_cert,
) )
except HTTPError, e: except HTTPError as e:
if e.response is not None: if e.response is not None:
response = e.response response = e.response
else: else:
raise raise
if options.print_headers: if options.print_headers:
print response.headers print(response.headers)
if options.print_body: if options.print_body:
print response.body print(response.body)
client.close() client.close()
if __name__ == "__main__": if __name__ == "__main__":
+34 -20
View File
@@ -24,24 +24,24 @@ This module also defines the `HTTPRequest` class which is exposed via
`tornado.web.RequestHandler.request`. `tornado.web.RequestHandler.request`.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import Cookie
import socket import socket
import ssl
import time import time
from tornado.escape import native_str, parse_qs_bytes from tornado.escape import native_str, parse_qs_bytes
from tornado import httputil from tornado import httputil
from tornado import iostream from tornado import iostream
from tornado.log import gen_log from tornado.log import gen_log
from tornado.netutil import TCPServer from tornado.tcpserver import TCPServer
from tornado import stack_context from tornado import stack_context
from tornado.util import b, bytes_type from tornado.util import bytes_type
try: try:
import ssl # Python 2.6+ import Cookie # py2
except ImportError: except ImportError:
ssl = None import http.cookies as Cookie # py3
class HTTPServer(TCPServer): class HTTPServer(TCPServer):
@@ -95,7 +95,8 @@ class HTTPServer(TCPServer):
`HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL. `HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
To make this server serve SSL traffic, send the ssl_options dictionary To make this server serve SSL traffic, send the ssl_options dictionary
argument with the arguments required for the `ssl.wrap_socket` method, argument with the arguments required for the `ssl.wrap_socket` method,
including "certfile" and "keyfile":: including "certfile" and "keyfile". In Python 3.2+ you can pass
an `ssl.SSLContext` object instead of a dict::
HTTPServer(applicaton, ssl_options={ HTTPServer(applicaton, ssl_options={
"certfile": os.path.join(data_dir, "mydomain.crt"), "certfile": os.path.join(data_dir, "mydomain.crt"),
@@ -103,9 +104,9 @@ class HTTPServer(TCPServer):
}) })
`HTTPServer` initialization follows one of three patterns (the `HTTPServer` initialization follows one of three patterns (the
initialization methods are defined on `tornado.netutil.TCPServer`): initialization methods are defined on `tornado.tcpserver.TCPServer`):
1. `~tornado.netutil.TCPServer.listen`: simple single-process:: 1. `~tornado.tcpserver.TCPServer.listen`: simple single-process::
server = HTTPServer(app) server = HTTPServer(app)
server.listen(8888) server.listen(8888)
@@ -114,7 +115,7 @@ class HTTPServer(TCPServer):
In many cases, `tornado.web.Application.listen` can be used to avoid In many cases, `tornado.web.Application.listen` can be used to avoid
the need to explicitly create the `HTTPServer`. the need to explicitly create the `HTTPServer`.
2. `~tornado.netutil.TCPServer.bind`/`~tornado.netutil.TCPServer.start`: 2. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`:
simple multi-process:: simple multi-process::
server = HTTPServer(app) server = HTTPServer(app)
@@ -126,7 +127,7 @@ class HTTPServer(TCPServer):
to the `HTTPServer` constructor. `start` will always start to the `HTTPServer` constructor. `start` will always start
the server on the default singleton `IOLoop`. the server on the default singleton `IOLoop`.
3. `~tornado.netutil.TCPServer.add_sockets`: advanced multi-process:: 3. `~tornado.tcpserver.TCPServer.add_sockets`: advanced multi-process::
sockets = tornado.netutil.bind_sockets(8888) sockets = tornado.netutil.bind_sockets(8888)
tornado.process.fork_processes(0) tornado.process.fork_processes(0)
@@ -171,6 +172,10 @@ class HTTPConnection(object):
xheaders=False, protocol=None): xheaders=False, protocol=None):
self.stream = stream self.stream = stream
self.address = address self.address = address
# Save the socket's address family now so we know how to
# interpret self.address even after the stream is closed
# and its socket attribute replaced with None.
self.address_family = stream.socket.family
self.request_callback = request_callback self.request_callback = request_callback
self.no_keep_alive = no_keep_alive self.no_keep_alive = no_keep_alive
self.xheaders = xheaders self.xheaders = xheaders
@@ -180,7 +185,19 @@ class HTTPConnection(object):
# Save stack context here, outside of any request. This keeps # Save stack context here, outside of any request. This keeps
# contexts from one request from leaking into the next. # contexts from one request from leaking into the next.
self._header_callback = stack_context.wrap(self._on_headers) self._header_callback = stack_context.wrap(self._on_headers)
self.stream.read_until(b("\r\n\r\n"), self._header_callback) self.stream.read_until(b"\r\n\r\n", self._header_callback)
self._write_callback = None
self._close_callback = None
def set_close_callback(self, callback):
self._close_callback = stack_context.wrap(callback)
self.stream.set_close_callback(self._on_connection_close)
def _on_connection_close(self):
callback = self._close_callback
self._close_callback = None
callback()
# Delete any unfinished callbacks to break up reference cycles.
self._write_callback = None self._write_callback = None
def close(self): def close(self):
@@ -241,7 +258,7 @@ class HTTPConnection(object):
# Use a try/except instead of checking stream.closed() # Use a try/except instead of checking stream.closed()
# directly, because in some cases the stream doesn't discover # directly, because in some cases the stream doesn't discover
# that it's closed until you try to read from it. # that it's closed until you try to read from it.
self.stream.read_until(b("\r\n\r\n"), self._header_callback) self.stream.read_until(b"\r\n\r\n", self._header_callback)
except iostream.StreamClosedError: except iostream.StreamClosedError:
self.close() self.close()
@@ -259,10 +276,7 @@ class HTTPConnection(object):
headers = httputil.HTTPHeaders.parse(data[eol:]) headers = httputil.HTTPHeaders.parse(data[eol:])
# HTTPRequest wants an IP, not a full socket address # HTTPRequest wants an IP, not a full socket address
if getattr(self.stream.socket, 'family', socket.AF_INET) in ( if self.address_family in (socket.AF_INET, socket.AF_INET6):
socket.AF_INET, socket.AF_INET6):
# Jython 2.5.2 doesn't have the socket.family attribute,
# so just assume IP in that case.
remote_ip = self.address[0] remote_ip = self.address[0]
else: else:
# Unix (or other) socket; fake the remote address # Unix (or other) socket; fake the remote address
@@ -278,12 +292,12 @@ class HTTPConnection(object):
if content_length > self.stream.max_buffer_size: if content_length > self.stream.max_buffer_size:
raise _BadRequestException("Content-Length too long") raise _BadRequestException("Content-Length too long")
if headers.get("Expect") == "100-continue": if headers.get("Expect") == "100-continue":
self.stream.write(b("HTTP/1.1 100 (Continue)\r\n\r\n")) self.stream.write(b"HTTP/1.1 100 (Continue)\r\n\r\n")
self.stream.read_bytes(content_length, self._on_request_body) self.stream.read_bytes(content_length, self._on_request_body)
return return
self.request_callback(self._request) self.request_callback(self._request)
except _BadRequestException, e: except _BadRequestException as e:
gen_log.info("Malformed HTTP request from %s: %s", gen_log.info("Malformed HTTP request from %s: %s",
self.address[0], e) self.address[0], e)
self.close() self.close()
@@ -484,7 +498,7 @@ class HTTPRequest(object):
socket.SOCK_STREAM, socket.SOCK_STREAM,
0, socket.AI_NUMERICHOST) 0, socket.AI_NUMERICHOST)
return bool(res) return bool(res)
except socket.gaierror, e: except socket.gaierror as e:
if e.args[0] == socket.EAI_NONAME: if e.args[0] == socket.EAI_NONAME:
return False return False
raise raise
+53 -19
View File
@@ -16,14 +16,26 @@
"""HTTP utility code shared by clients and servers.""" """HTTP utility code shared by clients and servers."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import urllib import datetime
import numbers
import re import re
import time
from tornado.escape import native_str, parse_qs_bytes, utf8 from tornado.escape import native_str, parse_qs_bytes, utf8
from tornado.log import gen_log from tornado.log import gen_log
from tornado.util import b, ObjectDict from tornado.util import ObjectDict
try:
from httplib import responses # py2
except ImportError:
from http.client import responses # py3
try:
from urllib import urlencode # py2
except ImportError:
from urllib.parse import urlencode # py3
class HTTPHeaders(dict): class HTTPHeaders(dict):
@@ -34,7 +46,7 @@ class HTTPHeaders(dict):
value per key, with multiple values joined by a comma. value per key, with multiple values joined by a comma.
>>> h = HTTPHeaders({"content-type": "text/html"}) >>> h = HTTPHeaders({"content-type": "text/html"})
>>> h.keys() >>> list(h.keys())
['Content-Type'] ['Content-Type']
>>> h["Content-Type"] >>> h["Content-Type"]
'text/html' 'text/html'
@@ -47,7 +59,7 @@ class HTTPHeaders(dict):
['A=B', 'C=D'] ['A=B', 'C=D']
>>> for (k,v) in sorted(h.get_all()): >>> for (k,v) in sorted(h.get_all()):
... print '%s: %s' % (k,v) ... print('%s: %s' % (k,v))
... ...
Content-Type: text/html Content-Type: text/html
Set-Cookie: A=B Set-Cookie: A=B
@@ -60,7 +72,7 @@ class HTTPHeaders(dict):
self._as_list = {} self._as_list = {}
self._last_key = None self._last_key = None
if (len(args) == 1 and len(kwargs) == 0 and if (len(args) == 1 and len(kwargs) == 0 and
isinstance(args[0], HTTPHeaders)): isinstance(args[0], HTTPHeaders)):
# Copy constructor # Copy constructor
for k, v in args[0].get_all(): for k, v in args[0].get_all():
self.add(k, v) self.add(k, v)
@@ -76,7 +88,9 @@ class HTTPHeaders(dict):
self._last_key = norm_name self._last_key = norm_name
if norm_name in self: if norm_name in self:
# bypass our override of __setitem__ since it modifies _as_list # bypass our override of __setitem__ since it modifies _as_list
dict.__setitem__(self, norm_name, self[norm_name] + ',' + value) dict.__setitem__(self, norm_name,
native_str(self[norm_name]) + ',' +
native_str(value))
self._as_list[norm_name].append(value) self._as_list[norm_name].append(value)
else: else:
self[norm_name] = value self[norm_name] = value
@@ -92,8 +106,8 @@ class HTTPHeaders(dict):
If a header has multiple values, multiple pairs will be If a header has multiple values, multiple pairs will be
returned with the same name. returned with the same name.
""" """
for name, list in self._as_list.iteritems(): for name, values in self._as_list.items():
for value in list: for value in values:
yield (name, value) yield (name, value)
def parse_line(self, line): def parse_line(self, line):
@@ -119,7 +133,7 @@ class HTTPHeaders(dict):
"""Returns a dictionary from HTTP header text. """Returns a dictionary from HTTP header text.
>>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n") >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
>>> sorted(h.iteritems()) >>> sorted(h.items())
[('Content-Length', '42'), ('Content-Type', 'text/html')] [('Content-Length', '42'), ('Content-Type', 'text/html')]
""" """
h = cls() h = cls()
@@ -152,7 +166,7 @@ class HTTPHeaders(dict):
def update(self, *args, **kwargs): def update(self, *args, **kwargs):
# dict.update bypasses our __setitem__ # dict.update bypasses our __setitem__
for k, v in dict(*args, **kwargs).iteritems(): for k, v in dict(*args, **kwargs).items():
self[k] = v self[k] = v
def copy(self): def copy(self):
@@ -191,7 +205,7 @@ def url_concat(url, args):
return url return url
if url[-1] not in ('?', '&'): if url[-1] not in ('?', '&'):
url += '&' if ('?' in url) else '?' url += '&' if ('?' in url) else '?'
return url + urllib.urlencode(args) return url + urlencode(args)
class HTTPFile(ObjectDict): class HTTPFile(ObjectDict):
@@ -216,7 +230,7 @@ def parse_body_arguments(content_type, body, arguments, files):
""" """
if content_type.startswith("application/x-www-form-urlencoded"): if content_type.startswith("application/x-www-form-urlencoded"):
uri_arguments = parse_qs_bytes(native_str(body)) uri_arguments = parse_qs_bytes(native_str(body))
for name, values in uri_arguments.iteritems(): for name, values in uri_arguments.items():
values = [v for v in values if v] values = [v for v in values if v]
if values: if values:
arguments.setdefault(name, []).extend(values) arguments.setdefault(name, []).extend(values)
@@ -243,24 +257,24 @@ def parse_multipart_form_data(boundary, data, arguments, files):
# xmpp). I think we're also supposed to handle backslash-escapes # xmpp). I think we're also supposed to handle backslash-escapes
# here but I'll save that until we see a client that uses them # here but I'll save that until we see a client that uses them
# in the wild. # in the wild.
if boundary.startswith(b('"')) and boundary.endswith(b('"')): if boundary.startswith(b'"') and boundary.endswith(b'"'):
boundary = boundary[1:-1] boundary = boundary[1:-1]
final_boundary_index = data.rfind(b("--") + boundary + b("--")) final_boundary_index = data.rfind(b"--" + boundary + b"--")
if final_boundary_index == -1: if final_boundary_index == -1:
gen_log.warning("Invalid multipart/form-data: no final boundary") gen_log.warning("Invalid multipart/form-data: no final boundary")
return return
parts = data[:final_boundary_index].split(b("--") + boundary + b("\r\n")) parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n")
for part in parts: for part in parts:
if not part: if not part:
continue continue
eoh = part.find(b("\r\n\r\n")) eoh = part.find(b"\r\n\r\n")
if eoh == -1: if eoh == -1:
gen_log.warning("multipart/form-data missing headers") gen_log.warning("multipart/form-data missing headers")
continue continue
headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
disp_header = headers.get("Content-Disposition", "") disp_header = headers.get("Content-Disposition", "")
disposition, disp_params = _parse_header(disp_header) disposition, disp_params = _parse_header(disp_header)
if disposition != "form-data" or not part.endswith(b("\r\n")): if disposition != "form-data" or not part.endswith(b"\r\n"):
gen_log.warning("Invalid multipart/form-data") gen_log.warning("Invalid multipart/form-data")
continue continue
value = part[eoh + 4:-2] value = part[eoh + 4:-2]
@@ -277,6 +291,26 @@ def parse_multipart_form_data(boundary, data, arguments, files):
arguments.setdefault(name, []).append(value) arguments.setdefault(name, []).append(value)
def format_timestamp(ts):
"""Formats a timestamp in the format used by HTTP.
The argument may be a numeric timestamp as returned by `time.time()`,
a time tuple as returned by `time.gmtime()`, or a `datetime.datetime`
object.
>>> format_timestamp(1359312200)
'Sun, 27 Jan 2013 18:43:20 GMT'
"""
if isinstance(ts, (tuple, time.struct_time)):
pass
elif isinstance(ts, datetime.datetime):
ts = ts.utctimetuple()
elif isinstance(ts, numbers.Real):
ts = time.gmtime(ts)
else:
raise TypeError("unknown timestamp type: %r" % ts)
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", ts)
# _parseparam and _parse_header are copied and modified from python2.7's cgi.py # _parseparam and _parse_header are copied and modified from python2.7's cgi.py
# The original 2.7 version of this code did not correctly support some # The original 2.7 version of this code did not correctly support some
# combinations of semicolons and double quotes. # combinations of semicolons and double quotes.
@@ -300,7 +334,7 @@ def _parse_header(line):
""" """
parts = _parseparam(';' + line) parts = _parseparam(';' + line)
key = parts.next() key = next(parts)
pdict = {} pdict = {}
for p in parts: for p in parts:
i = p.find('=') i = p.find('=')
+40 -36
View File
@@ -26,17 +26,16 @@ In addition to I/O events, the `IOLoop` can also schedule time-based events.
`IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`. `IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import datetime import datetime
import errno import errno
import functools import functools
import heapq import heapq
import logging import logging
import numbers
import os import os
import select import select
import sys
import thread
import threading import threading
import time import time
import traceback import traceback
@@ -56,6 +55,11 @@ try:
except ImportError: except ImportError:
futures = None futures = None
try:
import thread # py2
except ImportError:
import _thread as thread # py3
from tornado.platform.auto import set_close_exec, Waker from tornado.platform.auto import set_close_exec, Waker
@@ -66,7 +70,7 @@ class IOLoop(Configurable):
2.6+) if they are available, or else we fall back on select(). If 2.6+) if they are available, or else we fall back on select(). If
you are implementing a system that needs to handle thousands of you are implementing a system that needs to handle thousands of
simultaneous connections, you should use a system that supports either simultaneous connections, you should use a system that supports either
epoll or queue. epoll or kqueue.
Example usage for a simple TCP server:: Example usage for a simple TCP server::
@@ -177,13 +181,9 @@ class IOLoop(Configurable):
@classmethod @classmethod
def configurable_default(cls): def configurable_default(cls):
if hasattr(select, "epoll") or sys.platform.startswith('linux'): if hasattr(select, "epoll"):
try: from tornado.platform.epoll import EPollIOLoop
from tornado.platform.epoll import EPollIOLoop return EPollIOLoop
return EPollIOLoop
except ImportError:
gen_log.warning("unable to import EPollIOLoop, falling back to SelectIOLoop")
pass
if hasattr(select, "kqueue"): if hasattr(select, "kqueue"):
# Python 2.6+ on BSD or Mac # Python 2.6+ on BSD or Mac
from tornado.platform.kqueue import KQueueIOLoop from tornado.platform.kqueue import KQueueIOLoop
@@ -194,7 +194,7 @@ class IOLoop(Configurable):
def initialize(self): def initialize(self):
pass pass
def close(self, all_fds = False): def close(self, all_fds=False):
"""Closes the IOLoop, freeing any resources used. """Closes the IOLoop, freeing any resources used.
If ``all_fds`` is true, all file descriptors registered on the If ``all_fds`` is true, all file descriptors registered on the
@@ -252,8 +252,8 @@ class IOLoop(Configurable):
For use with set_blocking_signal_threshold. For use with set_blocking_signal_threshold.
""" """
gen_log.warning('IOLoop blocked for %f seconds in\n%s', gen_log.warning('IOLoop blocked for %f seconds in\n%s',
self._blocking_signal_threshold, self._blocking_signal_threshold,
''.join(traceback.format_stack(frame))) ''.join(traceback.format_stack(frame)))
def start(self): def start(self):
"""Starts the I/O loop. """Starts the I/O loop.
@@ -264,7 +264,8 @@ class IOLoop(Configurable):
raise NotImplementedError() raise NotImplementedError()
def stop(self): def stop(self):
"""Stop the loop after the current event loop iteration is complete. """Stop the I/O loop.
If the event loop is not currently running, the next call to start() If the event loop is not currently running, the next call to start()
will return immediately. will return immediately.
@@ -280,6 +281,8 @@ class IOLoop(Configurable):
Note that even after `stop` has been called, the IOLoop is not Note that even after `stop` has been called, the IOLoop is not
completely stopped until `IOLoop.start` has also returned. completely stopped until `IOLoop.start` has also returned.
Some work that was scheduled before the call to `stop` may still
be run before the IOLoop shuts down.
""" """
raise NotImplementedError() raise NotImplementedError()
@@ -316,7 +319,9 @@ class IOLoop(Configurable):
def remove_timeout(self, timeout): def remove_timeout(self, timeout):
"""Cancels a pending timeout. """Cancels a pending timeout.
The argument is a handle as returned by add_timeout. The argument is a handle as returned by add_timeout. It is
safe to call `remove_timeout` even if the callback has already
been run.
""" """
raise NotImplementedError() raise NotImplementedError()
@@ -351,6 +356,7 @@ class IOLoop(Configurable):
_FUTURE_TYPES = (futures.Future, DummyFuture) _FUTURE_TYPES = (futures.Future, DummyFuture)
else: else:
_FUTURE_TYPES = DummyFuture _FUTURE_TYPES = DummyFuture
def add_future(self, future, callback): def add_future(self, future, callback):
"""Schedules a callback on the IOLoop when the given future is finished. """Schedules a callback on the IOLoop when the given future is finished.
@@ -381,8 +387,7 @@ class IOLoop(Configurable):
The exception itself is not passed explicitly, but is available The exception itself is not passed explicitly, but is available
in sys.exc_info. in sys.exc_info.
""" """
app_log.error("Exception in callback %r", callback, exc_info = True) app_log.error("Exception in callback %r", callback, exc_info=True)
class PollIOLoop(IOLoop): class PollIOLoop(IOLoop):
@@ -392,7 +397,7 @@ class PollIOLoop(IOLoop):
(Linux), `tornado.platform.kqueue.KQueueIOLoop` (BSD and Mac), or (Linux), `tornado.platform.kqueue.KQueueIOLoop` (BSD and Mac), or
`tornado.platform.select.SelectIOLoop` (all platforms). `tornado.platform.select.SelectIOLoop` (all platforms).
""" """
def initialize(self, impl, time_func = None): def initialize(self, impl, time_func=None):
super(PollIOLoop, self).initialize() super(PollIOLoop, self).initialize()
self._impl = impl self._impl = impl
if hasattr(self._impl, 'fileno'): if hasattr(self._impl, 'fileno'):
@@ -416,16 +421,16 @@ class PollIOLoop(IOLoop):
lambda fd, events: self._waker.consume(), lambda fd, events: self._waker.consume(),
self.READ) self.READ)
def close(self, all_fds = False): def close(self, all_fds=False):
with self._callback_lock: with self._callback_lock:
self._closing = True self._closing = True
self.remove_handler(self._waker.fileno()) self.remove_handler(self._waker.fileno())
if all_fds: if all_fds:
for fd in self._handlers.keys()[:]: for fd in self._handlers.keys():
try: try:
os.close(fd) os.close(fd)
except Exception: except Exception:
gen_log.debug("error closing fd %s", fd, exc_info = True) gen_log.debug("error closing fd %s", fd, exc_info=True)
self._waker.close() self._waker.close()
self._impl.close() self._impl.close()
@@ -442,12 +447,12 @@ class PollIOLoop(IOLoop):
try: try:
self._impl.unregister(fd) self._impl.unregister(fd)
except Exception: except Exception:
gen_log.debug("Error deleting fd from IOLoop", exc_info = True) gen_log.debug("Error deleting fd from IOLoop", exc_info=True)
def set_blocking_signal_threshold(self, seconds, action): def set_blocking_signal_threshold(self, seconds, action):
if not hasattr(signal, "setitimer"): if not hasattr(signal, "setitimer"):
gen_log.error("set_blocking_signal_threshold requires a signal module " gen_log.error("set_blocking_signal_threshold requires a signal module "
"with the setitimer method") "with the setitimer method")
return return
self._blocking_signal_threshold = seconds self._blocking_signal_threshold = seconds
if seconds is not None: if seconds is not None:
@@ -500,7 +505,7 @@ class PollIOLoop(IOLoop):
# IOLoop is just started once at the beginning. # IOLoop is just started once at the beginning.
signal.set_wakeup_fd(old_wakeup_fd) signal.set_wakeup_fd(old_wakeup_fd)
old_wakeup_fd = None old_wakeup_fd = None
except ValueError: # non-main thread except ValueError: # non-main thread
pass pass
while True: while True:
@@ -543,7 +548,7 @@ class PollIOLoop(IOLoop):
try: try:
event_pairs = self._impl.poll(poll_timeout) event_pairs = self._impl.poll(poll_timeout)
except Exception, e: except Exception as e:
# Depending on python version and IOLoop implementation, # Depending on python version and IOLoop implementation,
# different exception types may be thrown and there are # different exception types may be thrown and there are
# two ways EINTR might be signaled: # two ways EINTR might be signaled:
@@ -568,18 +573,17 @@ class PollIOLoop(IOLoop):
while self._events: while self._events:
fd, events = self._events.popitem() fd, events = self._events.popitem()
try: try:
hdlr = self._handlers.get(fd) self._handlers[fd](fd, events)
if hdlr: hdlr(fd, events) except (OSError, IOError) as e:
except (OSError, IOError), e:
if e.args[0] == errno.EPIPE: if e.args[0] == errno.EPIPE:
# Happens when the client closes the connection # Happens when the client closes the connection
pass pass
else: else:
app_log.error("Exception in I/O handler for fd %s", app_log.error("Exception in I/O handler for fd %s",
fd, exc_info = True) fd, exc_info=True)
except Exception: except Exception:
app_log.error("Exception in I/O handler for fd %s", app_log.error("Exception in I/O handler for fd %s",
fd, exc_info = True) fd, exc_info=True)
# reset the stopped flag so another start/stop pair can be issued # reset the stopped flag so another start/stop pair can be issued
self._stopped = False self._stopped = False
if self._blocking_signal_threshold is not None: if self._blocking_signal_threshold is not None:
@@ -615,7 +619,7 @@ class PollIOLoop(IOLoop):
raise RuntimeError("IOLoop is closing") raise RuntimeError("IOLoop is closing")
list_empty = not self._callbacks list_empty = not self._callbacks
self._callbacks.append(functools.partial( self._callbacks.append(functools.partial(
stack_context.wrap(callback), *args, **kwargs)) stack_context.wrap(callback), *args, **kwargs))
if list_empty and thread.get_ident() != self._thread_ident: if list_empty and thread.get_ident() != self._thread_ident:
# If we're in the IOLoop's thread, we know it's not currently # If we're in the IOLoop's thread, we know it's not currently
# polling. If we're not, and we added the first callback to an # polling. If we're not, and we added the first callback to an
@@ -641,7 +645,7 @@ class PollIOLoop(IOLoop):
# either the old or new version of self._callbacks, # either the old or new version of self._callbacks,
# but either way will work. # but either way will work.
self._callbacks.append(functools.partial( self._callbacks.append(functools.partial(
stack_context.wrap(callback), *args, **kwargs)) stack_context.wrap(callback), *args, **kwargs))
class _Timeout(object): class _Timeout(object):
@@ -651,7 +655,7 @@ class _Timeout(object):
__slots__ = ['deadline', 'callback'] __slots__ = ['deadline', 'callback']
def __init__(self, deadline, callback, io_loop): def __init__(self, deadline, callback, io_loop):
if isinstance(deadline, (int, long, float)): if isinstance(deadline, numbers.Real):
self.deadline = deadline self.deadline = deadline
elif isinstance(deadline, datetime.timedelta): elif isinstance(deadline, datetime.timedelta):
self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline) self.deadline = io_loop.time() + _Timeout.timedelta_to_seconds(deadline)
@@ -684,7 +688,7 @@ class PeriodicCallback(object):
`start` must be called after the PeriodicCallback is created. `start` must be called after the PeriodicCallback is created.
""" """
def __init__(self, callback, callback_time, io_loop = None): def __init__(self, callback, callback_time, io_loop=None):
self.callback = callback self.callback = callback
if callback_time <= 0: if callback_time <= 0:
raise ValueError("Periodic callback must have a positive callback_time") raise ValueError("Periodic callback must have a positive callback_time")
@@ -712,7 +716,7 @@ class PeriodicCallback(object):
try: try:
self.callback() self.callback()
except Exception: except Exception:
app_log.error("Error in periodic callback", exc_info = True) app_log.error("Error in periodic callback", exc_info=True)
self._schedule_next() self._schedule_next()
def _schedule_next(self): def _schedule_next(self):
+72 -31
View File
@@ -24,33 +24,33 @@ Contents:
* `PipeIOStream`: Pipe-based IOStream implementation. * `PipeIOStream`: Pipe-based IOStream implementation.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import collections import collections
import errno import errno
import numbers
import os import os
import socket import socket
import ssl
import sys import sys
import re import re
from tornado import ioloop from tornado import ioloop
from tornado.log import gen_log, app_log from tornado.log import gen_log, app_log
from tornado.netutil import ssl_wrap_socket, ssl_match_hostname, SSLCertificateError
from tornado import stack_context from tornado import stack_context
from tornado.util import b, bytes_type from tornado.util import bytes_type
try:
import ssl # Python 2.6+
except ImportError:
ssl = None
try: try:
from tornado.platform.posix import _set_nonblocking from tornado.platform.posix import _set_nonblocking
except ImportError: except ImportError:
_set_nonblocking = None _set_nonblocking = None
class StreamClosedError(IOError): class StreamClosedError(IOError):
pass pass
class BaseIOStream(object): class BaseIOStream(object):
"""A utility class to write to and read from a non-blocking file or socket. """A utility class to write to and read from a non-blocking file or socket.
@@ -146,7 +146,7 @@ class BaseIOStream(object):
``callback`` will be empty. ``callback`` will be empty.
""" """
self._set_read_callback(callback) self._set_read_callback(callback)
assert isinstance(num_bytes, (int, long)) assert isinstance(num_bytes, numbers.Integral)
self._read_bytes = num_bytes self._read_bytes = num_bytes
self._streaming_callback = stack_context.wrap(streaming_callback) self._streaming_callback = stack_context.wrap(streaming_callback)
self._try_inline_read() self._try_inline_read()
@@ -237,12 +237,14 @@ class BaseIOStream(object):
def _maybe_run_close_callback(self): def _maybe_run_close_callback(self):
if (self.closed() and self._close_callback and if (self.closed() and self._close_callback and
self._pending_callbacks == 0): self._pending_callbacks == 0):
# if there are pending callbacks, don't run the close callback # if there are pending callbacks, don't run the close callback
# until they're done (see _maybe_add_error_handler) # until they're done (see _maybe_add_error_handler)
cb = self._close_callback cb = self._close_callback
self._close_callback = None self._close_callback = None
self._run_callback(cb) self._run_callback(cb)
# Delete any unfinished callbacks to break up reference cycles.
self._read_callback = self._write_callback = None
def reading(self): def reading(self):
"""Returns true if we are currently reading from the stream.""" """Returns true if we are currently reading from the stream."""
@@ -399,7 +401,7 @@ class BaseIOStream(object):
""" """
try: try:
chunk = self.read_from_fd() chunk = self.read_from_fd()
except (socket.error, IOError, OSError), e: except (socket.error, IOError, OSError) as e:
# ssl.SSLError is a subclass of socket.error # ssl.SSLError is a subclass of socket.error
if e.args[0] == errno.ECONNRESET: if e.args[0] == errno.ECONNRESET:
# Treat ECONNRESET as a connection close rather than # Treat ECONNRESET as a connection close rather than
@@ -504,7 +506,7 @@ class BaseIOStream(object):
self._write_buffer_frozen = False self._write_buffer_frozen = False
_merge_prefix(self._write_buffer, num_bytes) _merge_prefix(self._write_buffer, num_bytes)
self._write_buffer.popleft() self._write_buffer.popleft()
except socket.error, e: except socket.error as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
self._write_buffer_frozen = True self._write_buffer_frozen = True
break break
@@ -520,7 +522,7 @@ class BaseIOStream(object):
def _consume(self, loc): def _consume(self, loc):
if loc == 0: if loc == 0:
return b("") return b""
_merge_prefix(self._read_buffer, loc) _merge_prefix(self._read_buffer, loc)
self._read_buffer_size -= loc self._read_buffer_size -= loc
return self._read_buffer.popleft() return self._read_buffer.popleft()
@@ -630,7 +632,7 @@ class IOStream(BaseIOStream):
def read_from_fd(self): def read_from_fd(self):
try: try:
chunk = self.socket.recv(self.read_chunk_size) chunk = self.socket.recv(self.read_chunk_size)
except socket.error, e: except socket.error as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return None return None
else: else:
@@ -643,7 +645,7 @@ class IOStream(BaseIOStream):
def write_to_fd(self, data): def write_to_fd(self, data):
return self.socket.send(data) return self.socket.send(data)
def connect(self, address, callback=None): def connect(self, address, callback=None, server_hostname=None):
"""Connects the socket to a remote address without blocking. """Connects the socket to a remote address without blocking.
May only be called if the socket passed to the constructor was May only be called if the socket passed to the constructor was
@@ -652,6 +654,11 @@ class IOStream(BaseIOStream):
If callback is specified, it will be called when the If callback is specified, it will be called when the
connection is completed. connection is completed.
If specified, the ``server_hostname`` parameter will be used
in SSL connections for certificate validation (if requested in
the ``ssl_options``) and SNI (if supported; requires
Python 3.2+).
Note that it is safe to call IOStream.write while the Note that it is safe to call IOStream.write while the
connection is pending, in which case the data will be written connection is pending, in which case the data will be written
as soon as the connection is ready. Calling IOStream read as soon as the connection is ready. Calling IOStream read
@@ -661,7 +668,7 @@ class IOStream(BaseIOStream):
self._connecting = True self._connecting = True
try: try:
self.socket.connect(address) self.socket.connect(address)
except socket.error, e: except socket.error as e:
# In non-blocking mode we expect connect() to raise an # In non-blocking mode we expect connect() to raise an
# exception with EINPROGRESS or EWOULDBLOCK. # exception with EINPROGRESS or EWOULDBLOCK.
# #
@@ -710,8 +717,9 @@ class SSLIOStream(IOStream):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Creates an SSLIOStream. """Creates an SSLIOStream.
If a dictionary is provided as keyword argument ssl_options, The ``ssl_options`` keyword argument may either be a dictionary
it will be used as additional keyword arguments to ssl.wrap_socket. of keywords arguments for `ssl.wrap_socket`, or an `ssl.SSLContext`
object.
""" """
self._ssl_options = kwargs.pop('ssl_options', {}) self._ssl_options = kwargs.pop('ssl_options', {})
super(SSLIOStream, self).__init__(*args, **kwargs) super(SSLIOStream, self).__init__(*args, **kwargs)
@@ -719,6 +727,7 @@ class SSLIOStream(IOStream):
self._handshake_reading = False self._handshake_reading = False
self._handshake_writing = False self._handshake_writing = False
self._ssl_connect_callback = None self._ssl_connect_callback = None
self._server_hostname = None
def reading(self): def reading(self):
return self._handshake_reading or super(SSLIOStream, self).reading() return self._handshake_reading or super(SSLIOStream, self).reading()
@@ -732,7 +741,7 @@ class SSLIOStream(IOStream):
self._handshake_reading = False self._handshake_reading = False
self._handshake_writing = False self._handshake_writing = False
self.socket.do_handshake() self.socket.do_handshake()
except ssl.SSLError, err: except ssl.SSLError as err:
if err.args[0] == ssl.SSL_ERROR_WANT_READ: if err.args[0] == ssl.SSL_ERROR_WANT_READ:
self._handshake_reading = True self._handshake_reading = True
return return
@@ -751,16 +760,46 @@ class SSLIOStream(IOStream):
self.socket.fileno(), peer, err) self.socket.fileno(), peer, err)
return self.close(exc_info=True) return self.close(exc_info=True)
raise raise
except socket.error, err: except socket.error as err:
if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET): if err.args[0] in (errno.ECONNABORTED, errno.ECONNRESET):
return self.close(exc_info=True) return self.close(exc_info=True)
else: else:
self._ssl_accepting = False self._ssl_accepting = False
if not self._verify_cert(self.socket.getpeercert()):
self.close()
return
if self._ssl_connect_callback is not None: if self._ssl_connect_callback is not None:
callback = self._ssl_connect_callback callback = self._ssl_connect_callback
self._ssl_connect_callback = None self._ssl_connect_callback = None
self._run_callback(callback) self._run_callback(callback)
def _verify_cert(self, peercert):
"""Returns True if peercert is valid according to the configured
validation mode and hostname.
The ssl handshake already tested the certificate for a valid
CA signature; the only thing that remains is to check
the hostname.
"""
if isinstance(self._ssl_options, dict):
verify_mode = self._ssl_options.get('cert_reqs', ssl.CERT_NONE)
elif isinstance(self._ssl_options, ssl.SSLContext):
verify_mode = self._ssl_options.verify_mode
assert verify_mode in (ssl.CERT_NONE, ssl.CERT_REQUIRED, ssl.CERT_OPTIONAL)
if verify_mode == ssl.CERT_NONE or self._server_hostname is None:
return True
cert = self.socket.getpeercert()
if cert is None and verify_mode == ssl.CERT_REQUIRED:
gen_log.warning("No SSL certificate given")
return False
try:
ssl_match_hostname(peercert, self._server_hostname)
except SSLCertificateError:
gen_log.warning("Invalid SSL certificate", exc_info=True)
return False
else:
return True
def _handle_read(self): def _handle_read(self):
if self._ssl_accepting: if self._ssl_accepting:
self._do_ssl_handshake() self._do_ssl_handshake()
@@ -773,10 +812,11 @@ class SSLIOStream(IOStream):
return return
super(SSLIOStream, self)._handle_write() super(SSLIOStream, self)._handle_write()
def connect(self, address, callback=None): def connect(self, address, callback=None, server_hostname=None):
# Save the user's callback and run it after the ssl handshake # Save the user's callback and run it after the ssl handshake
# has completed. # has completed.
self._ssl_connect_callback = callback self._ssl_connect_callback = callback
self._server_hostname = server_hostname
super(SSLIOStream, self).connect(address, callback=None) super(SSLIOStream, self).connect(address, callback=None)
def _handle_connect(self): def _handle_connect(self):
@@ -786,9 +826,9 @@ class SSLIOStream(IOStream):
# user callbacks are enqueued asynchronously on the IOLoop, # user callbacks are enqueued asynchronously on the IOLoop,
# but since _handle_events calls _handle_connect immediately # but since _handle_events calls _handle_connect immediately
# followed by _handle_write we need this to be synchronous. # followed by _handle_write we need this to be synchronous.
self.socket = ssl.wrap_socket(self.socket, self.socket = ssl_wrap_socket(self.socket, self._ssl_options,
do_handshake_on_connect=False, server_hostname=self._server_hostname,
**self._ssl_options) do_handshake_on_connect=False)
super(SSLIOStream, self)._handle_connect() super(SSLIOStream, self)._handle_connect()
def read_from_fd(self): def read_from_fd(self):
@@ -804,14 +844,14 @@ class SSLIOStream(IOStream):
# called when there is nothing to read, so we have to use # called when there is nothing to read, so we have to use
# read() instead. # read() instead.
chunk = self.socket.read(self.read_chunk_size) chunk = self.socket.read(self.read_chunk_size)
except ssl.SSLError, e: except ssl.SSLError as e:
# SSLError is a subclass of socket.error, so this except # SSLError is a subclass of socket.error, so this except
# block must come first. # block must come first.
if e.args[0] == ssl.SSL_ERROR_WANT_READ: if e.args[0] == ssl.SSL_ERROR_WANT_READ:
return None return None
else: else:
raise raise
except socket.error, e: except socket.error as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return None return None
else: else:
@@ -821,6 +861,7 @@ class SSLIOStream(IOStream):
return None return None
return chunk return chunk
class PipeIOStream(BaseIOStream): class PipeIOStream(BaseIOStream):
"""Pipe-based IOStream implementation. """Pipe-based IOStream implementation.
@@ -844,7 +885,7 @@ class PipeIOStream(BaseIOStream):
def read_from_fd(self): def read_from_fd(self):
try: try:
chunk = os.read(self.fd, self.read_chunk_size) chunk = os.read(self.fd, self.read_chunk_size)
except (IOError, OSError), e: except (IOError, OSError) as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return None return None
elif e.args[0] == errno.EBADF: elif e.args[0] == errno.EBADF:
@@ -874,17 +915,17 @@ def _merge_prefix(deque, size):
string of up to size bytes. string of up to size bytes.
>>> d = collections.deque(['abc', 'de', 'fghi', 'j']) >>> d = collections.deque(['abc', 'de', 'fghi', 'j'])
>>> _merge_prefix(d, 5); print d >>> _merge_prefix(d, 5); print(d)
deque(['abcde', 'fghi', 'j']) deque(['abcde', 'fghi', 'j'])
Strings will be split as necessary to reach the desired size. Strings will be split as necessary to reach the desired size.
>>> _merge_prefix(d, 7); print d >>> _merge_prefix(d, 7); print(d)
deque(['abcdefg', 'hi', 'j']) deque(['abcdefg', 'hi', 'j'])
>>> _merge_prefix(d, 3); print d >>> _merge_prefix(d, 3); print(d)
deque(['abc', 'defg', 'hi', 'j']) deque(['abc', 'defg', 'hi', 'j'])
>>> _merge_prefix(d, 100); print d >>> _merge_prefix(d, 100); print(d)
deque(['abcdefghij']) deque(['abcdefghij'])
""" """
if len(deque) == 1 and len(deque[0]) <= size: if len(deque) == 1 and len(deque[0]) <= size:
@@ -904,7 +945,7 @@ def _merge_prefix(deque, size):
if prefix: if prefix:
deque.appendleft(type(prefix[0])().join(prefix)) deque.appendleft(type(prefix[0])().join(prefix))
if not deque: if not deque:
deque.appendleft(b("")) deque.appendleft(b"")
def doctests(): def doctests():
+78 -79
View File
@@ -39,7 +39,7 @@ supported by gettext and related tools). If neither method is called,
the locale.translate method will simply return the original string. the locale.translate method will simply return the original string.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import csv import csv
import datetime import datetime
@@ -48,6 +48,7 @@ import re
from tornado import escape from tornado import escape
from tornado.log import gen_log from tornado.log import gen_log
from tornado.util import u
_default_locale = "en_US" _default_locale = "en_US"
_translations = {} _translations = {}
@@ -80,11 +81,11 @@ def set_default_locale(code):
global _default_locale global _default_locale
global _supported_locales global _supported_locales
_default_locale = code _default_locale = code
_supported_locales = frozenset(_translations.keys() + [_default_locale]) _supported_locales = frozenset(list(_translations.keys()) + [_default_locale])
def load_translations(directory): def load_translations(directory):
u"""Loads translations from CSV files in a directory. u("""Loads translations from CSV files in a directory.
Translations are strings with optional Python-style named placeholders Translations are strings with optional Python-style named placeholders
(e.g., "My name is %(name)s") and their associated translations. (e.g., "My name is %(name)s") and their associated translations.
@@ -109,7 +110,7 @@ def load_translations(directory):
"%(name)s liked this","A %(name)s les gust\u00f3 esto","plural" "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural"
"%(name)s liked this","A %(name)s le gust\u00f3 esto","singular" "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular"
""" """)
global _translations global _translations
global _supported_locales global _supported_locales
_translations = {} _translations = {}
@@ -128,8 +129,6 @@ def load_translations(directory):
f = open(full_path, "r", encoding="utf-8") f = open(full_path, "r", encoding="utf-8")
except TypeError: except TypeError:
# python 2: files return byte strings, which are decoded below. # python 2: files return byte strings, which are decoded below.
# Once we drop python 2.5, this could use io.open instead
# on both 2 and 3.
f = open(full_path, "r") f = open(full_path, "r")
_translations[locale] = {} _translations[locale] = {}
for i, row in enumerate(csv.reader(f)): for i, row in enumerate(csv.reader(f)):
@@ -147,7 +146,7 @@ def load_translations(directory):
continue continue
_translations[locale].setdefault(plural, {})[english] = translation _translations[locale].setdefault(plural, {})[english] = translation
f.close() f.close()
_supported_locales = frozenset(_translations.keys() + [_default_locale]) _supported_locales = frozenset(list(_translations.keys()) + [_default_locale])
gen_log.debug("Supported locales: %s", sorted(_supported_locales)) gen_log.debug("Supported locales: %s", sorted(_supported_locales))
@@ -183,10 +182,10 @@ def load_gettext_translations(directory, domain):
os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain + ".mo")) os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain + ".mo"))
_translations[lang] = gettext.translation(domain, directory, _translations[lang] = gettext.translation(domain, directory,
languages=[lang]) languages=[lang])
except Exception, e: except Exception as e:
gen_log.error("Cannot load translation for '%s': %s", lang, str(e)) gen_log.error("Cannot load translation for '%s': %s", lang, str(e))
continue continue
_supported_locales = frozenset(_translations.keys() + [_default_locale]) _supported_locales = frozenset(list(_translations.keys()) + [_default_locale])
_use_gettext = True _use_gettext = True
gen_log.debug("Supported locales: %s", sorted(_supported_locales)) gen_log.debug("Supported locales: %s", sorted(_supported_locales))
@@ -242,7 +241,7 @@ class Locale(object):
def __init__(self, code, translations): def __init__(self, code, translations):
self.code = code self.code = code
self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown") self.name = LOCALE_NAMES.get(code, {}).get("name", u("Unknown"))
self.rtl = False self.rtl = False
for prefix in ["fa", "ar", "he"]: for prefix in ["fa", "ar", "he"]:
if self.code.startswith(prefix): if self.code.startswith(prefix):
@@ -323,26 +322,26 @@ class Locale(object):
if days == 0: if days == 0:
format = _("%(time)s") format = _("%(time)s")
elif days == 1 and local_date.day == local_yesterday.day and \ elif days == 1 and local_date.day == local_yesterday.day and \
relative: relative:
format = _("yesterday") if shorter else \ format = _("yesterday") if shorter else \
_("yesterday at %(time)s") _("yesterday at %(time)s")
elif days < 5: elif days < 5:
format = _("%(weekday)s") if shorter else \ format = _("%(weekday)s") if shorter else \
_("%(weekday)s at %(time)s") _("%(weekday)s at %(time)s")
elif days < 334: # 11mo, since confusing for same month last year elif days < 334: # 11mo, since confusing for same month last year
format = _("%(month_name)s %(day)s") if shorter else \ format = _("%(month_name)s %(day)s") if shorter else \
_("%(month_name)s %(day)s at %(time)s") _("%(month_name)s %(day)s at %(time)s")
if format is None: if format is None:
format = _("%(month_name)s %(day)s, %(year)s") if shorter else \ format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
_("%(month_name)s %(day)s, %(year)s at %(time)s") _("%(month_name)s %(day)s, %(year)s at %(time)s")
tfhour_clock = self.code not in ("en", "en_US", "zh_CN") tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
if tfhour_clock: if tfhour_clock:
str_time = "%d:%02d" % (local_date.hour, local_date.minute) str_time = "%d:%02d" % (local_date.hour, local_date.minute)
elif self.code == "zh_CN": elif self.code == "zh_CN":
str_time = "%s%d:%02d" % ( str_time = "%s%d:%02d" % (
(u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12], (u('\u4e0a\u5348'), u('\u4e0b\u5348'))[local_date.hour >= 12],
local_date.hour % 12 or 12, local_date.minute) local_date.hour % 12 or 12, local_date.minute)
else: else:
str_time = "%d:%02d %s" % ( str_time = "%d:%02d %s" % (
@@ -388,7 +387,7 @@ class Locale(object):
return "" return ""
if len(parts) == 1: if len(parts) == 1:
return parts[0] return parts[0]
comma = u' \u0648 ' if self.code.startswith("fa") else u", " comma = u(' \u0648 ') if self.code.startswith("fa") else u(", ")
return _("%(commas)s and %(last)s") % { return _("%(commas)s and %(last)s") % {
"commas": comma.join(parts[:-1]), "commas": comma.join(parts[:-1]),
"last": parts[len(parts) - 1], "last": parts[len(parts) - 1],
@@ -444,66 +443,66 @@ class GettextLocale(Locale):
return self.gettext(message) return self.gettext(message)
LOCALE_NAMES = { LOCALE_NAMES = {
"af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"}, "af_ZA": {"name_en": u("Afrikaans"), "name": u("Afrikaans")},
"am_ET": {"name_en": u"Amharic", "name": u'\u12a0\u121b\u122d\u129b'}, "am_ET": {"name_en": u("Amharic"), "name": u('\u12a0\u121b\u122d\u129b')},
"ar_AR": {"name_en": u"Arabic", "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"}, "ar_AR": {"name_en": u("Arabic"), "name": u("\u0627\u0644\u0639\u0631\u0628\u064a\u0629")},
"bg_BG": {"name_en": u"Bulgarian", "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"}, "bg_BG": {"name_en": u("Bulgarian"), "name": u("\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438")},
"bn_IN": {"name_en": u"Bengali", "name": u"\u09ac\u09be\u0982\u09b2\u09be"}, "bn_IN": {"name_en": u("Bengali"), "name": u("\u09ac\u09be\u0982\u09b2\u09be")},
"bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"}, "bs_BA": {"name_en": u("Bosnian"), "name": u("Bosanski")},
"ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"}, "ca_ES": {"name_en": u("Catalan"), "name": u("Catal\xe0")},
"cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"}, "cs_CZ": {"name_en": u("Czech"), "name": u("\u010ce\u0161tina")},
"cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"}, "cy_GB": {"name_en": u("Welsh"), "name": u("Cymraeg")},
"da_DK": {"name_en": u"Danish", "name": u"Dansk"}, "da_DK": {"name_en": u("Danish"), "name": u("Dansk")},
"de_DE": {"name_en": u"German", "name": u"Deutsch"}, "de_DE": {"name_en": u("German"), "name": u("Deutsch")},
"el_GR": {"name_en": u"Greek", "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"}, "el_GR": {"name_en": u("Greek"), "name": u("\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac")},
"en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"}, "en_GB": {"name_en": u("English (UK)"), "name": u("English (UK)")},
"en_US": {"name_en": u"English (US)", "name": u"English (US)"}, "en_US": {"name_en": u("English (US)"), "name": u("English (US)")},
"es_ES": {"name_en": u"Spanish (Spain)", "name": u"Espa\xf1ol (Espa\xf1a)"}, "es_ES": {"name_en": u("Spanish (Spain)"), "name": u("Espa\xf1ol (Espa\xf1a)")},
"es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"}, "es_LA": {"name_en": u("Spanish"), "name": u("Espa\xf1ol")},
"et_EE": {"name_en": u"Estonian", "name": u"Eesti"}, "et_EE": {"name_en": u("Estonian"), "name": u("Eesti")},
"eu_ES": {"name_en": u"Basque", "name": u"Euskara"}, "eu_ES": {"name_en": u("Basque"), "name": u("Euskara")},
"fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"}, "fa_IR": {"name_en": u("Persian"), "name": u("\u0641\u0627\u0631\u0633\u06cc")},
"fi_FI": {"name_en": u"Finnish", "name": u"Suomi"}, "fi_FI": {"name_en": u("Finnish"), "name": u("Suomi")},
"fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"}, "fr_CA": {"name_en": u("French (Canada)"), "name": u("Fran\xe7ais (Canada)")},
"fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"}, "fr_FR": {"name_en": u("French"), "name": u("Fran\xe7ais")},
"ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"}, "ga_IE": {"name_en": u("Irish"), "name": u("Gaeilge")},
"gl_ES": {"name_en": u"Galician", "name": u"Galego"}, "gl_ES": {"name_en": u("Galician"), "name": u("Galego")},
"he_IL": {"name_en": u"Hebrew", "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"}, "he_IL": {"name_en": u("Hebrew"), "name": u("\u05e2\u05d1\u05e8\u05d9\u05ea")},
"hi_IN": {"name_en": u"Hindi", "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"}, "hi_IN": {"name_en": u("Hindi"), "name": u("\u0939\u093f\u0928\u094d\u0926\u0940")},
"hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"}, "hr_HR": {"name_en": u("Croatian"), "name": u("Hrvatski")},
"hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"}, "hu_HU": {"name_en": u("Hungarian"), "name": u("Magyar")},
"id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"}, "id_ID": {"name_en": u("Indonesian"), "name": u("Bahasa Indonesia")},
"is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"}, "is_IS": {"name_en": u("Icelandic"), "name": u("\xcdslenska")},
"it_IT": {"name_en": u"Italian", "name": u"Italiano"}, "it_IT": {"name_en": u("Italian"), "name": u("Italiano")},
"ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"}, "ja_JP": {"name_en": u("Japanese"), "name": u("\u65e5\u672c\u8a9e")},
"ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"}, "ko_KR": {"name_en": u("Korean"), "name": u("\ud55c\uad6d\uc5b4")},
"lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"}, "lt_LT": {"name_en": u("Lithuanian"), "name": u("Lietuvi\u0173")},
"lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"}, "lv_LV": {"name_en": u("Latvian"), "name": u("Latvie\u0161u")},
"mk_MK": {"name_en": u"Macedonian", "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"}, "mk_MK": {"name_en": u("Macedonian"), "name": u("\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438")},
"ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"}, "ml_IN": {"name_en": u("Malayalam"), "name": u("\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02")},
"ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"}, "ms_MY": {"name_en": u("Malay"), "name": u("Bahasa Melayu")},
"nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"}, "nb_NO": {"name_en": u("Norwegian (bokmal)"), "name": u("Norsk (bokm\xe5l)")},
"nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"}, "nl_NL": {"name_en": u("Dutch"), "name": u("Nederlands")},
"nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"}, "nn_NO": {"name_en": u("Norwegian (nynorsk)"), "name": u("Norsk (nynorsk)")},
"pa_IN": {"name_en": u"Punjabi", "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"}, "pa_IN": {"name_en": u("Punjabi"), "name": u("\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40")},
"pl_PL": {"name_en": u"Polish", "name": u"Polski"}, "pl_PL": {"name_en": u("Polish"), "name": u("Polski")},
"pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"}, "pt_BR": {"name_en": u("Portuguese (Brazil)"), "name": u("Portugu\xeas (Brasil)")},
"pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"}, "pt_PT": {"name_en": u("Portuguese (Portugal)"), "name": u("Portugu\xeas (Portugal)")},
"ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"}, "ro_RO": {"name_en": u("Romanian"), "name": u("Rom\xe2n\u0103")},
"ru_RU": {"name_en": u"Russian", "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"}, "ru_RU": {"name_en": u("Russian"), "name": u("\u0420\u0443\u0441\u0441\u043a\u0438\u0439")},
"sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"}, "sk_SK": {"name_en": u("Slovak"), "name": u("Sloven\u010dina")},
"sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"}, "sl_SI": {"name_en": u("Slovenian"), "name": u("Sloven\u0161\u010dina")},
"sq_AL": {"name_en": u"Albanian", "name": u"Shqip"}, "sq_AL": {"name_en": u("Albanian"), "name": u("Shqip")},
"sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"}, "sr_RS": {"name_en": u("Serbian"), "name": u("\u0421\u0440\u043f\u0441\u043a\u0438")},
"sv_SE": {"name_en": u"Swedish", "name": u"Svenska"}, "sv_SE": {"name_en": u("Swedish"), "name": u("Svenska")},
"sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"}, "sw_KE": {"name_en": u("Swahili"), "name": u("Kiswahili")},
"ta_IN": {"name_en": u"Tamil", "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"}, "ta_IN": {"name_en": u("Tamil"), "name": u("\u0ba4\u0bae\u0bbf\u0bb4\u0bcd")},
"te_IN": {"name_en": u"Telugu", "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"}, "te_IN": {"name_en": u("Telugu"), "name": u("\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41")},
"th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"}, "th_TH": {"name_en": u("Thai"), "name": u("\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22")},
"tl_PH": {"name_en": u"Filipino", "name": u"Filipino"}, "tl_PH": {"name_en": u("Filipino"), "name": u("Filipino")},
"tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"}, "tr_TR": {"name_en": u("Turkish"), "name": u("T\xfcrk\xe7e")},
"uk_UA": {"name_en": u"Ukraini ", "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"}, "uk_UA": {"name_en": u("Ukraini "), "name": u("\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430")},
"vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"}, "vi_VN": {"name_en": u("Vietnamese"), "name": u("Ti\u1ebfng Vi\u1ec7t")},
"zh_CN": {"name_en": u"Chinese (Simplified)", "name": u"\u4e2d\u6587(\u7b80\u4f53)"}, "zh_CN": {"name_en": u("Chinese (Simplified)"), "name": u("\u4e2d\u6587(\u7b80\u4f53)")},
"zh_TW": {"name_en": u"Chinese (Traditional)", "name": u"\u4e2d\u6587(\u7e41\u9ad4)"}, "zh_TW": {"name_en": u("Chinese (Traditional)"), "name": u("\u4e2d\u6587(\u7e41\u9ad4)")},
} }
+48 -37
View File
@@ -28,13 +28,15 @@ These streams may be configured independently using the standard library's
`logging` module. For example, you may wish to send ``tornado.access`` logs `logging` module. For example, you may wish to send ``tornado.access`` logs
to a separate file for analysis. to a separate file for analysis.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import logging import logging
import logging.handlers
import sys import sys
import time import time
from tornado.escape import _unicode from tornado.escape import _unicode
from tornado.util import unicode_type, basestring_type
try: try:
import curses import curses
@@ -46,6 +48,7 @@ access_log = logging.getLogger("tornado.access")
app_log = logging.getLogger("tornado.application") app_log = logging.getLogger("tornado.application")
gen_log = logging.getLogger("tornado.general") gen_log = logging.getLogger("tornado.general")
def _stderr_supports_color(): def _stderr_supports_color():
color = False color = False
if curses and sys.stderr.isatty(): if curses and sys.stderr.isatty():
@@ -85,25 +88,25 @@ class LogFormatter(logging.Formatter):
fg_color = (curses.tigetstr("setaf") or fg_color = (curses.tigetstr("setaf") or
curses.tigetstr("setf") or "") curses.tigetstr("setf") or "")
if (3, 0) < sys.version_info < (3, 2, 3): if (3, 0) < sys.version_info < (3, 2, 3):
fg_color = unicode(fg_color, "ascii") fg_color = unicode_type(fg_color, "ascii")
self._colors = { self._colors = {
logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue logging.DEBUG: unicode_type(curses.tparm(fg_color, 4), # Blue
"ascii"), "ascii"),
logging.INFO: unicode(curses.tparm(fg_color, 2), # Green logging.INFO: unicode_type(curses.tparm(fg_color, 2), # Green
"ascii"), "ascii"),
logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow logging.WARNING: unicode_type(curses.tparm(fg_color, 3), # Yellow
"ascii"), "ascii"),
logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red logging.ERROR: unicode_type(curses.tparm(fg_color, 1), # Red
"ascii"), "ascii"),
} }
self._normal = unicode(curses.tigetstr("sgr0"), "ascii") self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
def format(self, record): def format(self, record):
try: try:
record.message = record.getMessage() record.message = record.getMessage()
except Exception, e: except Exception as e:
record.message = "Bad message (%r): %r" % (e, record.__dict__) record.message = "Bad message (%r): %r" % (e, record.__dict__)
assert isinstance(record.message, basestring) # guaranteed by logging assert isinstance(record.message, basestring_type) # guaranteed by logging
record.asctime = time.strftime( record.asctime = time.strftime(
"%y%m%d %H:%M:%S", self.converter(record.created)) "%y%m%d %H:%M:%S", self.converter(record.created))
prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \ prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \
@@ -128,20 +131,27 @@ class LogFormatter(logging.Formatter):
# it's worth it since the encoding errors that would otherwise # it's worth it since the encoding errors that would otherwise
# result are so useless (and tornado is fond of using utf8-encoded # result are so useless (and tornado is fond of using utf8-encoded
# byte strings whereever possible). # byte strings whereever possible).
try: def safe_unicode(s):
message = _unicode(record.message) try:
except UnicodeDecodeError: return _unicode(s)
message = repr(record.message) except UnicodeDecodeError:
return repr(s)
formatted = prefix + " " + message formatted = prefix + " " + safe_unicode(record.message)
if record.exc_info: if record.exc_info:
if not record.exc_text: if not record.exc_text:
record.exc_text = self.formatException(record.exc_info) record.exc_text = self.formatException(record.exc_info)
if record.exc_text: if record.exc_text:
formatted = formatted.rstrip() + "\n" + record.exc_text # exc_text contains multiple lines. We need to safe_unicode
# each line separately so that non-utf8 bytes don't cause
# all the newlines to turn into '\n'.
lines = [formatted.rstrip()]
lines.extend(safe_unicode(ln) for ln in record.exc_text.split('\n'))
formatted = '\n'.join(lines)
return formatted.replace("\n", "\n ") return formatted.replace("\n", "\n ")
def enable_pretty_logging(options=None):
def enable_pretty_logging(options=None, logger=None):
"""Turns on formatted logging output as configured. """Turns on formatted logging output as configured.
This is called automaticaly by `tornado.options.parse_command_line` This is called automaticaly by `tornado.options.parse_command_line`
@@ -151,22 +161,23 @@ def enable_pretty_logging(options=None):
from tornado.options import options from tornado.options import options
if options.logging == 'none': if options.logging == 'none':
return return
root_logger = logging.getLogger() if logger is None:
root_logger.setLevel(getattr(logging, options.logging.upper())) logger = logging.getLogger()
logger.setLevel(getattr(logging, options.logging.upper()))
if options.log_file_prefix: if options.log_file_prefix:
channel = logging.handlers.RotatingFileHandler( channel = logging.handlers.RotatingFileHandler(
filename=options.log_file_prefix, filename=options.log_file_prefix,
maxBytes=options.log_file_max_size, maxBytes=options.log_file_max_size,
backupCount=options.log_file_num_backups) backupCount=options.log_file_num_backups)
channel.setFormatter(LogFormatter(color=False)) channel.setFormatter(LogFormatter(color=False))
root_logger.addHandler(channel) logger.addHandler(channel)
if (options.log_to_stderr or if (options.log_to_stderr or
(options.log_to_stderr is None and not root_logger.handlers)): (options.log_to_stderr is None and not logger.handlers)):
# Set up color if we are in a tty and curses is installed # Set up color if we are in a tty and curses is installed
channel = logging.StreamHandler() channel = logging.StreamHandler()
channel.setFormatter(LogFormatter()) channel.setFormatter(LogFormatter())
root_logger.addHandler(channel) logger.addHandler(channel)
def define_logging_options(options=None): def define_logging_options(options=None):
@@ -174,21 +185,21 @@ def define_logging_options(options=None):
# late import to prevent cycle # late import to prevent cycle
from tornado.options import options from tornado.options import options
options.define("logging", default="info", options.define("logging", default="info",
help=("Set the Python log level. If 'none', tornado won't touch the " help=("Set the Python log level. If 'none', tornado won't touch the "
"logging configuration."), "logging configuration."),
metavar="debug|info|warning|error|none") metavar="debug|info|warning|error|none")
options.define("log_to_stderr", type=bool, default=None, options.define("log_to_stderr", type=bool, default=None,
help=("Send log output to stderr (colorized if possible). " help=("Send log output to stderr (colorized if possible). "
"By default use stderr if --log_file_prefix is not set and " "By default use stderr if --log_file_prefix is not set and "
"no other logging is configured.")) "no other logging is configured."))
options.define("log_file_prefix", type=str, default=None, metavar="PATH", options.define("log_file_prefix", type=str, default=None, metavar="PATH",
help=("Path prefix for log files. " help=("Path prefix for log files. "
"Note that if you are running multiple tornado processes, " "Note that if you are running multiple tornado processes, "
"log_file_prefix must be different for each of them (e.g. " "log_file_prefix must be different for each of them (e.g. "
"include the port number)")) "include the port number)"))
options.define("log_file_max_size", type=int, default=100 * 1000 * 1000, options.define("log_file_max_size", type=int, default=100 * 1000 * 1000,
help="max size of log files before rollover") help="max size of log files before rollover")
options.define("log_file_num_backups", type=int, default=10, options.define("log_file_num_backups", type=int, default=10,
help="number of log files to keep") help="number of log files to keep")
options.add_parse_callback(enable_pretty_logging) options.add_parse_callback(enable_pretty_logging)
+192 -217
View File
@@ -16,226 +16,19 @@
"""Miscellaneous network utility code.""" """Miscellaneous network utility code."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import errno import errno
import os import os
import re
import socket import socket
import ssl
import stat import stat
from tornado import process
from tornado.concurrent import dummy_executor, run_on_executor from tornado.concurrent import dummy_executor, run_on_executor
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream
from tornado.log import app_log
from tornado.platform.auto import set_close_exec from tornado.platform.auto import set_close_exec
from tornado.util import Configurable
try:
import ssl # Python 2.6+
except ImportError:
ssl = None
class TCPServer(object):
r"""A non-blocking, single-threaded TCP server.
To use `TCPServer`, define a subclass which overrides the `handle_stream`
method.
`TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
To make this server serve SSL traffic, send the ssl_options dictionary
argument with the arguments required for the `ssl.wrap_socket` method,
including "certfile" and "keyfile"::
TCPServer(ssl_options={
"certfile": os.path.join(data_dir, "mydomain.crt"),
"keyfile": os.path.join(data_dir, "mydomain.key"),
})
`TCPServer` initialization follows one of three patterns:
1. `listen`: simple single-process::
server = TCPServer()
server.listen(8888)
IOLoop.instance().start()
2. `bind`/`start`: simple multi-process::
server = TCPServer()
server.bind(8888)
server.start(0) # Forks multiple sub-processes
IOLoop.instance().start()
When using this interface, an `IOLoop` must *not* be passed
to the `TCPServer` constructor. `start` will always start
the server on the default singleton `IOLoop`.
3. `add_sockets`: advanced multi-process::
sockets = bind_sockets(8888)
tornado.process.fork_processes(0)
server = TCPServer()
server.add_sockets(sockets)
IOLoop.instance().start()
The `add_sockets` interface is more complicated, but it can be
used with `tornado.process.fork_processes` to give you more
flexibility in when the fork happens. `add_sockets` can
also be used in single-process servers if you want to create
your listening sockets in some way other than
`bind_sockets`.
"""
def __init__(self, io_loop=None, ssl_options=None):
self.io_loop = io_loop
self.ssl_options = ssl_options
self._sockets = {} # fd -> socket object
self._pending_sockets = []
self._started = False
# Verify the SSL options. Otherwise we don't get errors until clients
# connect. This doesn't verify that the keys are legitimate, but
# the SSL module doesn't do that until there is a connected socket
# which seems like too much work
if self.ssl_options is not None:
# Only certfile is required: it can contain both keys
if 'certfile' not in self.ssl_options:
raise KeyError('missing key "certfile" in ssl_options')
if not os.path.exists(self.ssl_options['certfile']):
raise ValueError('certfile "%s" does not exist' %
self.ssl_options['certfile'])
if ('keyfile' in self.ssl_options and
not os.path.exists(self.ssl_options['keyfile'])):
raise ValueError('keyfile "%s" does not exist' %
self.ssl_options['keyfile'])
def listen(self, port, address=""):
"""Starts accepting connections on the given port.
This method may be called more than once to listen on multiple ports.
`listen` takes effect immediately; it is not necessary to call
`TCPServer.start` afterwards. It is, however, necessary to start
the `IOLoop`.
"""
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets)
def add_sockets(self, sockets):
"""Makes this server start accepting connections on the given sockets.
The ``sockets`` parameter is a list of socket objects such as
those returned by `bind_sockets`.
`add_sockets` is typically used in combination with that
method and `tornado.process.fork_processes` to provide greater
control over the initialization of a multi-process server.
"""
if self.io_loop is None:
self.io_loop = IOLoop.instance()
for sock in sockets:
self._sockets[sock.fileno()] = sock
add_accept_handler(sock, self._handle_connection,
io_loop=self.io_loop)
def add_socket(self, socket):
"""Singular version of `add_sockets`. Takes a single socket object."""
self.add_sockets([socket])
def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128):
"""Binds this server to the given port on the given address.
To start the server, call `start`. If you want to run this server
in a single process, you can call `listen` as a shortcut to the
sequence of `bind` and `start` calls.
Address may be either an IP address or hostname. If it's a hostname,
the server will listen on all IP addresses associated with the
name. Address may be an empty string or None to listen on all
available interfaces. Family may be set to either ``socket.AF_INET``
or ``socket.AF_INET6`` to restrict to ipv4 or ipv6 addresses, otherwise
both will be used if available.
The ``backlog`` argument has the same meaning as for
`socket.listen`.
This method may be called multiple times prior to `start` to listen
on multiple ports or interfaces.
"""
sockets = bind_sockets(port, address=address, family=family,
backlog=backlog)
if self._started:
self.add_sockets(sockets)
else:
self._pending_sockets.extend(sockets)
def start(self, num_processes=1):
"""Starts this server in the IOLoop.
By default, we run the server in this process and do not fork any
additional child process.
If num_processes is ``None`` or <= 0, we detect the number of cores
available on this machine and fork that number of child
processes. If num_processes is given and > 1, we fork that
specific number of sub-processes.
Since we use processes and not threads, there is no shared memory
between any server code.
Note that multiple processes are not compatible with the autoreload
module (or the ``debug=True`` option to `tornado.web.Application`).
When using multiple processes, no IOLoops can be created or
referenced until after the call to ``TCPServer.start(n)``.
"""
assert not self._started
self._started = True
if num_processes != 1:
process.fork_processes(num_processes)
sockets = self._pending_sockets
self._pending_sockets = []
self.add_sockets(sockets)
def stop(self):
"""Stops listening for new connections.
Requests currently in progress may still continue after the
server is stopped.
"""
for fd, sock in self._sockets.iteritems():
self.io_loop.remove_handler(fd)
sock.close()
def handle_stream(self, stream, address):
"""Override to handle a new `IOStream` from an incoming connection."""
raise NotImplementedError()
def _handle_connection(self, connection, address):
if self.ssl_options is not None:
assert ssl, "Python 2.6+ and OpenSSL required for SSL"
try:
connection = ssl.wrap_socket(connection,
server_side=True,
do_handshake_on_connect=False,
**self.ssl_options)
except ssl.SSLError, err:
if err.args[0] == ssl.SSL_ERROR_EOF:
return connection.close()
else:
raise
except socket.error, err:
if err.args[0] == errno.ECONNABORTED:
return connection.close()
else:
raise
try:
if self.ssl_options is not None:
stream = SSLIOStream(connection, io_loop=self.io_loop)
else:
stream = IOStream(connection, io_loop=self.io_loop)
self.handle_stream(stream, address)
except Exception:
app_log.error("Error in connection callback", exc_info=True)
def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None):
@@ -261,10 +54,17 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags
sockets = [] sockets = []
if address == "": if address == "":
address = None address = None
if not socket.has_ipv6 and family == socket.AF_UNSPEC:
# Python can be compiled with --disable-ipv6, which causes
# operations on AF_INET6 sockets to fail, but does not
# automatically exclude those results from getaddrinfo
# results.
# http://bugs.python.org/issue16208
family = socket.AF_INET
if flags is None: if flags is None:
flags = socket.AI_PASSIVE flags = socket.AI_PASSIVE
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
0, flags)): 0, flags)):
af, socktype, proto, canonname, sockaddr = res af, socktype, proto, canonname, sockaddr = res
sock = socket.socket(af, socktype, proto) sock = socket.socket(af, socktype, proto)
set_close_exec(sock.fileno()) set_close_exec(sock.fileno())
@@ -288,7 +88,7 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags
return sockets return sockets
if hasattr(socket, 'AF_UNIX'): if hasattr(socket, 'AF_UNIX'):
def bind_unix_socket(file, mode=0600, backlog=128): def bind_unix_socket(file, mode=0o600, backlog=128):
"""Creates a listening unix socket. """Creates a listening unix socket.
If a socket with the given name already exists, it will be deleted. If a socket with the given name already exists, it will be deleted.
@@ -304,7 +104,7 @@ if hasattr(socket, 'AF_UNIX'):
sock.setblocking(0) sock.setblocking(0)
try: try:
st = os.stat(file) st = os.stat(file)
except OSError, err: except OSError as err:
if err.errno != errno.ENOENT: if err.errno != errno.ENOENT:
raise raise
else: else:
@@ -334,7 +134,7 @@ def add_accept_handler(sock, callback, io_loop=None):
while True: while True:
try: try:
connection, address = sock.accept() connection, address = sock.accept()
except socket.error, e: except socket.error as e:
if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN): if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
return return
raise raise
@@ -342,11 +142,186 @@ def add_accept_handler(sock, callback, io_loop=None):
io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ) io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
class Resolver(object): class Resolver(Configurable):
def __init__(self, io_loop=None, executor=None): @classmethod
def configurable_base(cls):
return Resolver
@classmethod
def configurable_default(cls):
return BlockingResolver
def getaddrinfo(self, *args, **kwargs):
"""Resolves an address.
The arguments to this function are the same as to
`socket.getaddrinfo`, with the addition of an optional
keyword-only ``callback`` argument.
Returns a `Future` whose result is the same as the return
value of `socket.getaddrinfo`. If a callback is passed,
it will be run with the `Future` as an argument when it
is complete.
"""
raise NotImplementedError()
class ExecutorResolver(Resolver):
def initialize(self, io_loop=None, executor=None):
self.io_loop = io_loop or IOLoop.instance() self.io_loop = io_loop or IOLoop.instance()
self.executor = executor or dummy_executor self.executor = executor or dummy_executor
@run_on_executor @run_on_executor
def getaddrinfo(self, *args, **kwargs): def getaddrinfo(self, *args, **kwargs):
return socket.getaddrinfo(*args, **kwargs) return socket.getaddrinfo(*args, **kwargs)
class BlockingResolver(ExecutorResolver):
def initialize(self, io_loop=None):
super(BlockingResolver, self).initialize(io_loop=io_loop)
class ThreadedResolver(ExecutorResolver):
def initialize(self, io_loop=None, num_threads=10):
from concurrent.futures import ThreadPoolExecutor
super(ThreadedResolver, self).initialize(
io_loop=io_loop, executor=ThreadPoolExecutor(num_threads))
class OverrideResolver(Resolver):
"""Wraps a resolver with a mapping of overrides.
This can be used to make local DNS changes (e.g. for testing)
without modifying system-wide settings.
The mapping can contain either host strings or host-port pairs.
"""
def initialize(self, resolver, mapping):
self.resolver = resolver
self.mapping = mapping
def getaddrinfo(self, host, port, *args, **kwargs):
if (host, port) in self.mapping:
host, port = self.mapping[(host, port)]
elif host in self.mapping:
host = self.mapping[host]
return self.resolver.getaddrinfo(host, port, *args, **kwargs)
# These are the keyword arguments to ssl.wrap_socket that must be translated
# to their SSLContext equivalents (the other arguments are still passed
# to SSLContext.wrap_socket).
_SSL_CONTEXT_KEYWORDS = frozenset(['ssl_version', 'certfile', 'keyfile',
'cert_reqs', 'ca_certs', 'ciphers'])
def ssl_options_to_context(ssl_options):
"""Try to Convert an ssl_options dictionary to an SSLContext object.
The ``ssl_options`` dictionary contains keywords to be passed to
`ssl.wrap_sockets`. In Python 3.2+, `ssl.SSLContext` objects can
be used instead. This function converts the dict form to its
`SSLContext` equivalent, and may be used when a component which
accepts both forms needs to upgrade to the `SSLContext` version
to use features like SNI or NPN.
"""
if isinstance(ssl_options, dict):
assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options
if (not hasattr(ssl, 'SSLContext') or
isinstance(ssl_options, ssl.SSLContext)):
return ssl_options
context = ssl.SSLContext(
ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23))
if 'certfile' in ssl_options:
context.load_cert_chain(ssl_options['certfile'], ssl_options.get('keyfile', None))
if 'cert_reqs' in ssl_options:
context.verify_mode = ssl_options['cert_reqs']
if 'ca_certs' in ssl_options:
context.load_verify_locations(ssl_options['ca_certs'])
if 'ciphers' in ssl_options:
context.set_ciphers(ssl_options['ciphers'])
return context
def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs):
"""Returns an `ssl.SSLSocket` wrapping the given socket.
``ssl_options`` may be either a dictionary (as accepted by
`ssl_options_to_context) or an `ssl.SSLContext` object.
Additional keyword arguments are passed to `wrap_socket`
(either the `SSLContext` method or the `ssl` module function
as appropriate).
"""
context = ssl_options_to_context(ssl_options)
if hasattr(ssl, 'SSLContext') and isinstance(context, ssl.SSLContext):
if server_hostname is not None and getattr(ssl, 'HAS_SNI'):
# Python doesn't have server-side SNI support so we can't
# really unittest this, but it can be manually tested with
# python3.2 -m tornado.httpclient https://sni.velox.ch
return context.wrap_socket(socket, server_hostname=server_hostname,
**kwargs)
else:
return context.wrap_socket(socket, **kwargs)
else:
return ssl.wrap_socket(socket, **dict(context, **kwargs))
if hasattr(ssl, 'match_hostname'): # python 3.2+
ssl_match_hostname = ssl.match_hostname
SSLCertificateError = ssl.CertificateError
else:
# match_hostname was added to the standard library ssl module in python 3.2.
# The following code was backported for older releases and copied from
# https://bitbucket.org/brandon/backports.ssl_match_hostname
class SSLCertificateError(ValueError):
pass
def _dnsname_to_pat(dn):
pats = []
for frag in dn.split(r'.'):
if frag == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
else:
# Otherwise, '*' matches any dotless fragment.
frag = re.escape(frag)
pats.append(frag.replace(r'\*', '[^.]*'))
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
def ssl_match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
are mostly followed, but IP addresses are not accepted for *hostname*.
CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if not san:
# The subject is only checked when subjectAltName is empty
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise SSLCertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise SSLCertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise SSLCertificateError("no appropriate commonName or "
"subjectAltName fields were found")
+15 -11
View File
@@ -57,7 +57,7 @@ simply call methods on it. You may create additional `OptionParser`
instances to define isolated sets of options, such as for subcommands. instances to define isolated sets of options, such as for subcommands.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import datetime import datetime
import re import re
@@ -68,6 +68,7 @@ import textwrap
from tornado.escape import _unicode from tornado.escape import _unicode
from tornado.log import define_logging_options from tornado.log import define_logging_options
from tornado import stack_context from tornado import stack_context
from tornado.util import basestring_type
class Error(Exception): class Error(Exception):
@@ -171,7 +172,7 @@ class OptionParser(object):
if args is None: if args is None:
args = sys.argv args = sys.argv
remaining = [] remaining = []
for i in xrange(1, len(args)): for i in range(1, len(args)):
# All things after the last option are command line arguments # All things after the last option are command line arguments
if not args[i].startswith("-"): if not args[i].startswith("-"):
remaining = args[i:] remaining = args[i:]
@@ -218,15 +219,15 @@ class OptionParser(object):
"""Prints all the command line options to stderr (or another file).""" """Prints all the command line options to stderr (or another file)."""
if file is None: if file is None:
file = sys.stderr file = sys.stderr
print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] print("Usage: %s [OPTIONS]" % sys.argv[0], file=file)
print >> file, "\nOptions:\n" print("\nOptions:\n", file=file)
by_group = {} by_group = {}
for option in self._options.itervalues(): for option in self._options.values():
by_group.setdefault(option.group_name, []).append(option) by_group.setdefault(option.group_name, []).append(option)
for filename, o in sorted(by_group.items()): for filename, o in sorted(by_group.items()):
if filename: if filename:
print >> file, "\n%s options:\n" % os.path.normpath(filename) print("\n%s options:\n" % os.path.normpath(filename), file=file)
o.sort(key=lambda option: option.name) o.sort(key=lambda option: option.name)
for option in o: for option in o:
prefix = option.name prefix = option.name
@@ -238,10 +239,10 @@ class OptionParser(object):
lines = textwrap.wrap(description, 79 - 35) lines = textwrap.wrap(description, 79 - 35)
if len(prefix) > 30 or len(lines) == 0: if len(prefix) > 30 or len(lines) == 0:
lines.insert(0, '') lines.insert(0, '')
print >> file, " --%-30s %s" % (prefix, lines[0]) print(" --%-30s %s" % (prefix, lines[0]), file=file)
for line in lines[1:]: for line in lines[1:]:
print >> file, "%-34s %s" % (' ', line) print("%-34s %s" % (' ', line), file=file)
print >> file print(file=file)
def _help_callback(self, value): def _help_callback(self, value):
if value: if value:
@@ -272,6 +273,7 @@ class OptionParser(object):
""" """
return _Mockable(self) return _Mockable(self)
class _Mockable(object): class _Mockable(object):
"""`mock.patch` compatible wrapper for `OptionParser`. """`mock.patch` compatible wrapper for `OptionParser`.
@@ -300,8 +302,9 @@ class _Mockable(object):
def __delattr__(self, name): def __delattr__(self, name):
setattr(self._options, name, self._originals.pop(name)) setattr(self._options, name, self._originals.pop(name))
class _Option(object): class _Option(object):
def __init__(self, name, default=None, type=basestring, help=None, def __init__(self, name, default=None, type=basestring_type, help=None,
metavar=None, multiple=False, file_name=None, group_name=None, metavar=None, multiple=False, file_name=None, group_name=None,
callback=None): callback=None):
if default is None and multiple: if default is None and multiple:
@@ -325,7 +328,7 @@ class _Option(object):
datetime.datetime: self._parse_datetime, datetime.datetime: self._parse_datetime,
datetime.timedelta: self._parse_timedelta, datetime.timedelta: self._parse_timedelta,
bool: self._parse_bool, bool: self._parse_bool,
basestring: self._parse_string, basestring_type: self._parse_string,
}.get(self.type, self.type) }.get(self.type, self.type)
if self.multiple: if self.multiple:
self._value = [] self._value = []
@@ -467,6 +470,7 @@ def print_help(file=None):
""" """
return options.print_help(file) return options.print_help(file)
def add_parse_callback(callback): def add_parse_callback(callback):
"""Adds a parse callback, to be invoked when option parsing is done. """Adds a parse callback, to be invoked when option parsing is done.
+1 -1
View File
@@ -23,7 +23,7 @@ Most code that needs access to this functionality should do e.g.::
from tornado.platform.auto import set_close_exec from tornado.platform.auto import set_close_exec
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import os import os
+4 -5
View File
@@ -1,11 +1,10 @@
"""Lowest-common-denominator implementations of platform functionality.""" """Lowest-common-denominator implementations of platform functionality."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import errno import errno
import socket import socket
from tornado.platform import interface from tornado.platform import interface
from tornado.util import b
class Waker(interface.Waker): class Waker(interface.Waker):
@@ -43,9 +42,9 @@ class Waker(interface.Waker):
try: try:
self.writer.connect(connect_address) self.writer.connect(connect_address)
break # success break # success
except socket.error, detail: except socket.error as detail:
if (not hasattr(errno, 'WSAEADDRINUSE') or if (not hasattr(errno, 'WSAEADDRINUSE') or
detail[0] != errno.WSAEADDRINUSE): detail[0] != errno.WSAEADDRINUSE):
# "Address already in use" is the only error # "Address already in use" is the only error
# I've seen on two WinXP Pro SP2 boxes, under # I've seen on two WinXP Pro SP2 boxes, under
# Pythons 2.3.5 and 2.4.1. # Pythons 2.3.5 and 2.4.1.
@@ -74,7 +73,7 @@ class Waker(interface.Waker):
def wake(self): def wake(self):
try: try:
self.writer.send(b("x")) self.writer.send(b"x")
except (IOError, socket.error): except (IOError, socket.error):
pass pass
+5 -47
View File
@@ -13,56 +13,14 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""EPoll-based IOLoop implementation for Linux systems. """EPoll-based IOLoop implementation for Linux systems."""
from __future__ import absolute_import, division, print_function, with_statement
Supports the standard library's `select.epoll` function for Python 2.6+,
and our own C module for Python 2.5.
"""
from __future__ import absolute_import, division, with_statement
import os
import select import select
from tornado.ioloop import PollIOLoop from tornado.ioloop import PollIOLoop
if hasattr(select, 'epoll'):
# Python 2.6+
class EPollIOLoop(PollIOLoop):
def initialize(self, **kwargs):
super(EPollIOLoop, self).initialize(impl=select.epoll(), **kwargs)
else:
# Python 2.5
from tornado import epoll
class _EPoll(object):
"""An epoll-based event loop using our C module for Python 2.5 systems"""
_EPOLL_CTL_ADD = 1
_EPOLL_CTL_DEL = 2
_EPOLL_CTL_MOD = 3
def __init__(self):
self._epoll_fd = epoll.epoll_create()
def fileno(self):
return self._epoll_fd
def close(self):
os.close(self._epoll_fd)
def register(self, fd, events):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_ADD, fd, events)
def modify(self, fd, events):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_MOD, fd, events)
def unregister(self, fd):
epoll.epoll_ctl(self._epoll_fd, self._EPOLL_CTL_DEL, fd, 0)
def poll(self, timeout):
return epoll.epoll_wait(self._epoll_fd, int(timeout * 1000))
class EPollIOLoop(PollIOLoop):
def initialize(self, **kwargs):
super(EPollIOLoop, self).initialize(impl=_EPoll(), **kwargs)
class EPollIOLoop(PollIOLoop):
def initialize(self, **kwargs):
super(EPollIOLoop, self).initialize(impl=select.epoll(), **kwargs)
+1 -1
View File
@@ -21,7 +21,7 @@ for other tornado.platform modules. Most code should import the appropriate
implementation from `tornado.platform.auto`. implementation from `tornado.platform.auto`.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
def set_close_exec(fd): def set_close_exec(fd):
+4 -3
View File
@@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""KQueue-based IOLoop implementation for BSD/Mac systems.""" """KQueue-based IOLoop implementation for BSD/Mac systems."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import select import select
@@ -22,6 +22,7 @@ from tornado.ioloop import IOLoop, PollIOLoop
assert hasattr(select, 'kqueue'), 'kqueue not supported' assert hasattr(select, 'kqueue'), 'kqueue not supported'
class _KQueue(object): class _KQueue(object):
"""A kqueue-based event loop for BSD/Mac systems.""" """A kqueue-based event loop for BSD/Mac systems."""
def __init__(self): def __init__(self):
@@ -52,11 +53,11 @@ class _KQueue(object):
kevents = [] kevents = []
if events & IOLoop.WRITE: if events & IOLoop.WRITE:
kevents.append(select.kevent( kevents.append(select.kevent(
fd, filter=select.KQ_FILTER_WRITE, flags=flags)) fd, filter=select.KQ_FILTER_WRITE, flags=flags))
if events & IOLoop.READ or not kevents: if events & IOLoop.READ or not kevents:
# Always read when there is not a write # Always read when there is not a write
kevents.append(select.kevent( kevents.append(select.kevent(
fd, filter=select.KQ_FILTER_READ, flags=flags)) fd, filter=select.KQ_FILTER_READ, flags=flags))
# Even though control() takes a list, it seems to return EINVAL # Even though control() takes a list, it seems to return EINVAL
# on Mac OS X (10.6) when there is more than one event in the list. # on Mac OS X (10.6) when there is more than one event in the list.
for kevent in kevents: for kevent in kevents:
+2 -3
View File
@@ -16,13 +16,12 @@
"""Posix implementations of platform-specific functionality.""" """Posix implementations of platform-specific functionality."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import fcntl import fcntl
import os import os
from tornado.platform import interface from tornado.platform import interface
from tornado.util import b
def set_close_exec(fd): def set_close_exec(fd):
@@ -53,7 +52,7 @@ class Waker(interface.Waker):
def wake(self): def wake(self):
try: try:
self.writer.write(b("x")) self.writer.write(b"x")
except IOError: except IOError:
pass pass
+3 -2
View File
@@ -17,12 +17,13 @@
Used as a fallback for systems that don't support epoll or kqueue. Used as a fallback for systems that don't support epoll or kqueue.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import select import select
from tornado.ioloop import IOLoop, PollIOLoop from tornado.ioloop import IOLoop, PollIOLoop
class _Select(object): class _Select(object):
"""A simple, select()-based IOLoop implementation for non-Linux systems""" """A simple, select()-based IOLoop implementation for non-Linux systems"""
def __init__(self): def __init__(self):
@@ -69,7 +70,7 @@ class _Select(object):
events[fd] = events.get(fd, 0) | IOLoop.ERROR events[fd] = events.get(fd, 0) | IOLoop.ERROR
return events.items() return events.items()
class SelectIOLoop(PollIOLoop): class SelectIOLoop(PollIOLoop):
def initialize(self, **kwargs): def initialize(self, **kwargs):
super(SelectIOLoop, self).initialize(impl=_Select(), **kwargs) super(SelectIOLoop, self).initialize(impl=_Select(), **kwargs)
+12 -12
View File
@@ -64,11 +64,10 @@ reactor. Recommended usage::
This module has been tested with Twisted versions 11.0.0 and newer. This module has been tested with Twisted versions 11.0.0 and newer.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import functools import functools
import datetime import datetime
import time
from twisted.internet.posixbase import PosixReactorBase from twisted.internet.posixbase import PosixReactorBase
from twisted.internet.interfaces import \ from twisted.internet.interfaces import \
@@ -85,6 +84,7 @@ from tornado.stack_context import NullContext, wrap
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
@implementer(IDelayedCall)
class TornadoDelayedCall(object): class TornadoDelayedCall(object):
"""DelayedCall object for Tornado.""" """DelayedCall object for Tornado."""
def __init__(self, reactor, seconds, f, *args, **kw): def __init__(self, reactor, seconds, f, *args, **kw):
@@ -125,10 +125,9 @@ class TornadoDelayedCall(object):
def active(self): def active(self):
return self._active return self._active
# Fake class decorator for python 2.5 compatibility
TornadoDelayedCall = implementer(IDelayedCall)(TornadoDelayedCall)
@implementer(IReactorTime, IReactorFDSet)
class TornadoReactor(PosixReactorBase): class TornadoReactor(PosixReactorBase):
"""Twisted reactor built on the Tornado IOLoop. """Twisted reactor built on the Tornado IOLoop.
@@ -235,7 +234,7 @@ class TornadoReactor(PosixReactorBase):
with NullContext(): with NullContext():
self._fds[fd] = (reader, None) self._fds[fd] = (reader, None)
self._io_loop.add_handler(fd, self._invoke_callback, self._io_loop.add_handler(fd, self._invoke_callback,
IOLoop.READ) IOLoop.READ)
def addWriter(self, writer): def addWriter(self, writer):
"""Add a FileDescriptor for notification of data available to write.""" """Add a FileDescriptor for notification of data available to write."""
@@ -254,7 +253,7 @@ class TornadoReactor(PosixReactorBase):
with NullContext(): with NullContext():
self._fds[fd] = (None, writer) self._fds[fd] = (None, writer)
self._io_loop.add_handler(fd, self._invoke_callback, self._io_loop.add_handler(fd, self._invoke_callback,
IOLoop.WRITE) IOLoop.WRITE)
def removeReader(self, reader): def removeReader(self, reader):
"""Remove a Selectable for notification of data available to read.""" """Remove a Selectable for notification of data available to read."""
@@ -316,7 +315,6 @@ class TornadoReactor(PosixReactorBase):
def mainLoop(self): def mainLoop(self):
self._io_loop.start() self._io_loop.start()
TornadoReactor = implementer(IReactorTime, IReactorFDSet)(TornadoReactor)
class _TestReactor(TornadoReactor): class _TestReactor(TornadoReactor):
@@ -352,6 +350,8 @@ def install(io_loop=None):
installReactor(reactor) installReactor(reactor)
return reactor return reactor
@implementer(IReadDescriptor, IWriteDescriptor)
class _FD(object): class _FD(object):
def __init__(self, fd, handler): def __init__(self, fd, handler):
self.fd = fd self.fd = fd
@@ -378,7 +378,7 @@ class _FD(object):
def logPrefix(self): def logPrefix(self):
return '' return ''
_FD = implementer(IReadDescriptor, IWriteDescriptor)(_FD)
class TwistedIOLoop(tornado.ioloop.IOLoop): class TwistedIOLoop(tornado.ioloop.IOLoop):
"""IOLoop implementation that runs on Twisted. """IOLoop implementation that runs on Twisted.
@@ -405,15 +405,15 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
if fd in self.fds: if fd in self.fds:
raise ValueError('fd %d added twice' % fd) raise ValueError('fd %d added twice' % fd)
self.fds[fd] = _FD(fd, wrap(handler)) self.fds[fd] = _FD(fd, wrap(handler))
if events | tornado.ioloop.IOLoop.READ: if events & tornado.ioloop.IOLoop.READ:
self.fds[fd].reading = True self.fds[fd].reading = True
self.reactor.addReader(self.fds[fd]) self.reactor.addReader(self.fds[fd])
if events | tornado.ioloop.IOLoop.WRITE: if events & tornado.ioloop.IOLoop.WRITE:
self.fds[fd].writing = True self.fds[fd].writing = True
self.reactor.addWriter(self.fds[fd]) self.reactor.addWriter(self.fds[fd])
def update_handler(self, fd, events): def update_handler(self, fd, events):
if events | tornado.ioloop.IOLoop.READ: if events & tornado.ioloop.IOLoop.READ:
if not self.fds[fd].reading: if not self.fds[fd].reading:
self.fds[fd].reading = True self.fds[fd].reading = True
self.reactor.addReader(self.fds[fd]) self.reactor.addReader(self.fds[fd])
@@ -421,7 +421,7 @@ class TwistedIOLoop(tornado.ioloop.IOLoop):
if self.fds[fd].reading: if self.fds[fd].reading:
self.fds[fd].reading = False self.fds[fd].reading = False
self.reactor.removeReader(self.fds[fd]) self.reactor.removeReader(self.fds[fd])
if events | tornado.ioloop.IOLoop.WRITE: if events & tornado.ioloop.IOLoop.WRITE:
if not self.fds[fd].writing: if not self.fds[fd].writing:
self.fds[fd].writing = True self.fds[fd].writing = True
self.reactor.addWriter(self.fds[fd]) self.reactor.addWriter(self.fds[fd])
+1 -1
View File
@@ -2,7 +2,7 @@
# for production use. # for production use.
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import ctypes import ctypes
import ctypes.wintypes import ctypes.wintypes
+14 -14
View File
@@ -16,10 +16,10 @@
"""Utilities for working with multiple processes.""" """Utilities for working with multiple processes."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import errno import errno
import functools import multiprocessing
import os import os
import signal import signal
import subprocess import subprocess
@@ -34,18 +34,17 @@ from tornado.log import gen_log
from tornado import stack_context from tornado import stack_context
try: try:
import multiprocessing # Python 2.6+ long # py2
except ImportError: except NameError:
multiprocessing = None long = int # py3
def cpu_count(): def cpu_count():
"""Returns the number of processors on this machine.""" """Returns the number of processors on this machine."""
if multiprocessing is not None: try:
try: return multiprocessing.cpu_count()
return multiprocessing.cpu_count() except NotImplementedError:
except NotImplementedError: pass
pass
try: try:
return os.sysconf("SC_NPROCESSORS_CONF") return os.sysconf("SC_NPROCESSORS_CONF")
except ValueError: except ValueError:
@@ -125,7 +124,7 @@ def fork_processes(num_processes, max_restarts=100):
while children: while children:
try: try:
pid, status = os.wait() pid, status = os.wait()
except OSError, e: except OSError as e:
if e.errno == errno.EINTR: if e.errno == errno.EINTR:
continue continue
raise raise
@@ -162,6 +161,7 @@ def task_id():
global _task_id global _task_id
return _task_id return _task_id
class Subprocess(object): class Subprocess(object):
"""Wraps ``subprocess.Popen`` with IOStream support. """Wraps ``subprocess.Popen`` with IOStream support.
@@ -195,7 +195,7 @@ class Subprocess(object):
err_r, err_w = os.pipe() err_r, err_w = os.pipe()
kwargs['stderr'] = err_w kwargs['stderr'] = err_w
to_close.append(err_w) to_close.append(err_w)
self.stdout = PipeIOStream(err_r, io_loop=self.io_loop) self.stderr = PipeIOStream(err_r, io_loop=self.io_loop)
self.proc = subprocess.Popen(*args, **kwargs) self.proc = subprocess.Popen(*args, **kwargs)
for fd in to_close: for fd in to_close:
os.close(fd) os.close(fd)
@@ -253,14 +253,14 @@ class Subprocess(object):
@classmethod @classmethod
def _cleanup(cls): def _cleanup(cls):
for pid in cls._waiting.keys(): for pid in list(cls._waiting.keys()): # make a copy
cls._try_cleanup_process(pid) cls._try_cleanup_process(pid)
@classmethod @classmethod
def _try_cleanup_process(cls, pid): def _try_cleanup_process(cls, pid):
try: try:
ret_pid, status = os.waitpid(pid, os.WNOHANG) ret_pid, status = os.waitpid(pid, os.WNOHANG)
except OSError, e: except OSError as e:
if e.args[0] == errno.ECHILD: if e.args[0] == errno.ECHILD:
return return
if ret_pid == 0: if ret_pid == 0:
+55 -132
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
from tornado.escape import utf8, _unicode, native_str from tornado.escape import utf8, _unicode, native_str
from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy from tornado.httpclient import HTTPRequest, HTTPResponse, HTTPError, AsyncHTTPClient, main, _RequestProxy
from tornado.httputil import HTTPHeaders from tornado.httputil import HTTPHeaders
from tornado.iostream import IOStream, SSLIOStream from tornado.iostream import IOStream, SSLIOStream
from tornado.netutil import Resolver from tornado.netutil import Resolver, OverrideResolver
from tornado.log import gen_log from tornado.log import gen_log
from tornado import stack_context from tornado import stack_context
from tornado.util import b, GzipDecompressor from tornado.util import GzipDecompressor
import base64 import base64
import collections import collections
@@ -17,9 +17,8 @@ import functools
import os.path import os.path
import re import re
import socket import socket
import ssl
import sys import sys
import time
import urlparse
try: try:
from io import BytesIO # python 3 from io import BytesIO # python 3
@@ -27,9 +26,9 @@ except ImportError:
from cStringIO import StringIO as BytesIO # python 2 from cStringIO import StringIO as BytesIO # python 2
try: try:
import ssl # python 2.6+ import urlparse # py2
except ImportError: except ImportError:
ssl = None import urllib.parse as urlparse # py3
_DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt' _DEFAULT_CA_CERTS = os.path.dirname(__file__) + '/ca-certificates.crt'
@@ -45,12 +44,8 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
supported. In particular, proxies are not supported, connections supported. In particular, proxies are not supported, connections
are not reused, and callers cannot select the network interface to be are not reused, and callers cannot select the network interface to be
used. used.
Python 2.6 or higher is required for HTTPS support. Users of Python 2.5
should use the curl-based AsyncHTTPClient if HTTPS support is required.
""" """
def initialize(self, io_loop=None, max_clients=10, def initialize(self, io_loop, max_clients=10,
hostname_mapping=None, max_buffer_size=104857600, hostname_mapping=None, max_buffer_size=104857600,
resolver=None, defaults=None): resolver=None, defaults=None):
"""Creates a AsyncHTTPClient. """Creates a AsyncHTTPClient.
@@ -72,32 +67,24 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
max_buffer_size is the number of bytes that can be read by IOStream. It max_buffer_size is the number of bytes that can be read by IOStream. It
defaults to 100mb. defaults to 100mb.
""" """
self.io_loop = io_loop super(SimpleAsyncHTTPClient, self).initialize(io_loop,
defaults=defaults)
self.max_clients = max_clients self.max_clients = max_clients
self.queue = collections.deque() self.queue = collections.deque()
self.active = {} self.active = {}
self.hostname_mapping = hostname_mapping
self.max_buffer_size = max_buffer_size self.max_buffer_size = max_buffer_size
self.resolver = resolver or Resolver(io_loop=io_loop) self.resolver = resolver or Resolver(io_loop=io_loop)
self.defaults = dict(HTTPRequest._DEFAULTS) if hostname_mapping is not None:
if defaults is not None: self.resolver = OverrideResolver(resolver=self.resolver,
self.defaults.update(defaults) mapping=hostname_mapping)
def fetch(self, request, callback, **kwargs): def fetch_impl(self, request, callback):
if not isinstance(request, HTTPRequest):
request = HTTPRequest(url=request, **kwargs)
# We're going to modify this (to add Host, Accept-Encoding, etc),
# so make sure we don't modify the caller's object. This is also
# where normal dicts get converted to HTTPHeaders objects.
request.headers = HTTPHeaders(request.headers)
request = _RequestProxy(request, self.defaults)
callback = stack_context.wrap(callback)
self.queue.append((request, callback)) self.queue.append((request, callback))
self._process_queue() self._process_queue()
if self.queue: if self.queue:
gen_log.debug("max_clients limit reached, request queued. " gen_log.debug("max_clients limit reached, request queued. "
"%d active, %d queued requests." % ( "%d active, %d queued requests." % (
len(self.active), len(self.queue))) len(self.active), len(self.queue)))
def _process_queue(self): def _process_queue(self):
with stack_context.NullContext(): with stack_context.NullContext():
@@ -108,7 +95,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
_HTTPConnection(self.io_loop, self, request, _HTTPConnection(self.io_loop, self, request,
functools.partial(self._release_fetch, key), functools.partial(self._release_fetch, key),
callback, callback,
self.max_buffer_size) self.max_buffer_size, self.resolver)
def _release_fetch(self, key): def _release_fetch(self, key):
del self.active[key] del self.active[key]
@@ -119,7 +106,7 @@ class _HTTPConnection(object):
_SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
def __init__(self, io_loop, client, request, release_callback, def __init__(self, io_loop, client, request, release_callback,
final_callback, max_buffer_size): final_callback, max_buffer_size, resolver):
self.start_time = io_loop.time() self.start_time = io_loop.time()
self.io_loop = io_loop self.io_loop = io_loop
self.client = client self.client = client
@@ -127,6 +114,7 @@ class _HTTPConnection(object):
self.release_callback = release_callback self.release_callback = release_callback
self.final_callback = final_callback self.final_callback = final_callback
self.max_buffer_size = max_buffer_size self.max_buffer_size = max_buffer_size
self.resolver = resolver
self.code = None self.code = None
self.headers = None self.headers = None
self.chunks = None self.chunks = None
@@ -135,9 +123,6 @@ class _HTTPConnection(object):
self._timeout = None self._timeout = None
with stack_context.ExceptionStackContext(self._handle_exception): with stack_context.ExceptionStackContext(self._handle_exception):
self.parsed = urlparse.urlsplit(_unicode(self.request.url)) self.parsed = urlparse.urlsplit(_unicode(self.request.url))
if ssl is None and self.parsed.scheme == "https":
raise ValueError("HTTPS requires either python2.6+ or "
"curl_httpclient")
if self.parsed.scheme not in ("http", "https"): if self.parsed.scheme not in ("http", "https"):
raise ValueError("Unsupported url scheme: %s" % raise ValueError("Unsupported url scheme: %s" %
self.request.url) self.request.url)
@@ -157,8 +142,6 @@ class _HTTPConnection(object):
# raw ipv6 addresses in urls are enclosed in brackets # raw ipv6 addresses in urls are enclosed in brackets
host = host[1:-1] host = host[1:-1]
self.parsed_hostname = host # save final host for _on_connect self.parsed_hostname = host # save final host for _on_connect
if self.client.hostname_mapping is not None:
host = self.client.hostname_mapping.get(host, host)
if request.allow_ipv6: if request.allow_ipv6:
af = socket.AF_UNSPEC af = socket.AF_UNSPEC
@@ -167,7 +150,7 @@ class _HTTPConnection(object):
# so restrict to ipv4 by default. # so restrict to ipv4 by default.
af = socket.AF_INET af = socket.AF_INET
self.client.resolver.getaddrinfo( self.resolver.getaddrinfo(
host, port, af, socket.SOCK_STREAM, 0, 0, host, port, af, socket.SOCK_STREAM, 0, 0,
callback=self._on_resolve) callback=self._on_resolve)
@@ -220,31 +203,29 @@ class _HTTPConnection(object):
self.start_time + timeout, self.start_time + timeout,
stack_context.wrap(self._on_timeout)) stack_context.wrap(self._on_timeout))
self.stream.set_close_callback(self._on_close) self.stream.set_close_callback(self._on_close)
self.stream.connect(sockaddr, self._on_connect) # ipv6 addresses are broken (in self.parsed.hostname) until
# 2.7, here is correctly parsed value calculated in __init__
self.stream.connect(sockaddr, self._on_connect,
server_hostname=self.parsed_hostname)
def _on_timeout(self): def _on_timeout(self):
self._timeout = None self._timeout = None
if self.final_callback is not None: if self.final_callback is not None:
raise HTTPError(599, "Timeout") raise HTTPError(599, "Timeout")
def _on_connect(self): def _remove_timeout(self):
if self._timeout is not None: if self._timeout is not None:
self.io_loop.remove_timeout(self._timeout) self.io_loop.remove_timeout(self._timeout)
self._timeout = None self._timeout = None
def _on_connect(self):
self._remove_timeout()
if self.request.request_timeout: if self.request.request_timeout:
self._timeout = self.io_loop.add_timeout( self._timeout = self.io_loop.add_timeout(
self.start_time + self.request.request_timeout, self.start_time + self.request.request_timeout,
stack_context.wrap(self._on_timeout)) stack_context.wrap(self._on_timeout))
if (self.request.validate_cert and
isinstance(self.stream, SSLIOStream)):
match_hostname(self.stream.socket.getpeercert(),
# ipv6 addresses are broken (in
# self.parsed.hostname) until 2.7, here is
# correctly parsed value calculated in
# __init__
self.parsed_hostname)
if (self.request.method not in self._SUPPORTED_METHODS and if (self.request.method not in self._SUPPORTED_METHODS and
not self.request.allow_nonstandard_methods): not self.request.allow_nonstandard_methods):
raise KeyError("unknown method %s" % self.request.method) raise KeyError("unknown method %s" % self.request.method)
for key in ('network_interface', for key in ('network_interface',
'proxy_host', 'proxy_port', 'proxy_host', 'proxy_port',
@@ -265,8 +246,8 @@ class _HTTPConnection(object):
username = self.request.auth_username username = self.request.auth_username
password = self.request.auth_password or '' password = self.request.auth_password or ''
if username is not None: if username is not None:
auth = utf8(username) + b(":") + utf8(password) auth = utf8(username) + b":" + utf8(password)
self.request.headers["Authorization"] = (b("Basic ") + self.request.headers["Authorization"] = (b"Basic " +
base64.b64encode(auth)) base64.b64encode(auth))
if self.request.user_agent: if self.request.user_agent:
self.request.headers["User-Agent"] = self.request.user_agent self.request.headers["User-Agent"] = self.request.user_agent
@@ -277,25 +258,25 @@ class _HTTPConnection(object):
assert self.request.body is None assert self.request.body is None
if self.request.body is not None: if self.request.body is not None:
self.request.headers["Content-Length"] = str(len( self.request.headers["Content-Length"] = str(len(
self.request.body)) self.request.body))
if (self.request.method == "POST" and if (self.request.method == "POST" and
"Content-Type" not in self.request.headers): "Content-Type" not in self.request.headers):
self.request.headers["Content-Type"] = "application/x-www-form-urlencoded" self.request.headers["Content-Type"] = "application/x-www-form-urlencoded"
if self.request.use_gzip: if self.request.use_gzip:
self.request.headers["Accept-Encoding"] = "gzip" self.request.headers["Accept-Encoding"] = "gzip"
req_path = ((self.parsed.path or '/') + req_path = ((self.parsed.path or '/') +
(('?' + self.parsed.query) if self.parsed.query else '')) (('?' + self.parsed.query) if self.parsed.query else ''))
request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method, request_lines = [utf8("%s %s HTTP/1.1" % (self.request.method,
req_path))] req_path))]
for k, v in self.request.headers.get_all(): for k, v in self.request.headers.get_all():
line = utf8(k) + b(": ") + utf8(v) line = utf8(k) + b": " + utf8(v)
if b('\n') in line: if b'\n' in line:
raise ValueError('Newline in header: ' + repr(line)) raise ValueError('Newline in header: ' + repr(line))
request_lines.append(line) request_lines.append(line)
self.stream.write(b("\r\n").join(request_lines) + b("\r\n\r\n")) self.stream.write(b"\r\n".join(request_lines) + b"\r\n\r\n")
if self.request.body is not None: if self.request.body is not None:
self.stream.write(self.request.body) self.stream.write(self.request.body)
self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers) self.stream.read_until_regex(b"\r?\n\r?\n", self._on_headers)
def _release(self): def _release(self):
if self.release_callback is not None: if self.release_callback is not None:
@@ -312,10 +293,11 @@ class _HTTPConnection(object):
def _handle_exception(self, typ, value, tb): def _handle_exception(self, typ, value, tb):
if self.final_callback: if self.final_callback:
self._remove_timeout()
gen_log.warning("uncaught exception", exc_info=(typ, value, tb)) gen_log.warning("uncaught exception", exc_info=(typ, value, tb))
self._run_callback(HTTPResponse(self.request, 599, error=value, self._run_callback(HTTPResponse(self.request, 599, error=value,
request_time=self.io_loop.time() - self.start_time, request_time=self.io_loop.time() - self.start_time,
)) ))
if hasattr(self, "stream"): if hasattr(self, "stream"):
self.stream.close() self.stream.close()
@@ -334,19 +316,22 @@ class _HTTPConnection(object):
message = str(self.stream.error) message = str(self.stream.error)
raise HTTPError(599, message) raise HTTPError(599, message)
def _handle_1xx(self, code):
self.stream.read_until_regex(b"\r?\n\r?\n", self._on_headers)
def _on_headers(self, data): def _on_headers(self, data):
data = native_str(data.decode("latin1")) data = native_str(data.decode("latin1"))
first_line, _, header_data = data.partition("\n") first_line, _, header_data = data.partition("\n")
match = re.match("HTTP/1.[01] ([0-9]+) ([^\r]*)", first_line) match = re.match("HTTP/1.[01] ([0-9]+) ([^\r]*)", first_line)
assert match assert match
code = int(match.group(1)) code = int(match.group(1))
self.headers = HTTPHeaders.parse(header_data)
if 100 <= code < 200: if 100 <= code < 200:
self.stream.read_until_regex(b("\r?\n\r?\n"), self._on_headers) self._handle_1xx(code)
return return
else: else:
self.code = code self.code = code
self.reason = match.group(2) self.reason = match.group(2)
self.headers = HTTPHeaders.parse(header_data)
if "Content-Length" in self.headers: if "Content-Length" in self.headers:
if "," in self.headers["Content-Length"]: if "," in self.headers["Content-Length"]:
@@ -372,38 +357,36 @@ class _HTTPConnection(object):
if self.request.method == "HEAD" or self.code == 304: if self.request.method == "HEAD" or self.code == 304:
# HEAD requests and 304 responses never have content, even # HEAD requests and 304 responses never have content, even
# though they may have content-length headers # though they may have content-length headers
self._on_body(b("")) self._on_body(b"")
return return
if 100 <= self.code < 200 or self.code == 204: if 100 <= self.code < 200 or self.code == 204:
# These response codes never have bodies # These response codes never have bodies
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.3
if ("Transfer-Encoding" in self.headers or if ("Transfer-Encoding" in self.headers or
content_length not in (None, 0)): content_length not in (None, 0)):
raise ValueError("Response with code %d should not have body" % raise ValueError("Response with code %d should not have body" %
self.code) self.code)
self._on_body(b("")) self._on_body(b"")
return return
if (self.request.use_gzip and if (self.request.use_gzip and
self.headers.get("Content-Encoding") == "gzip"): self.headers.get("Content-Encoding") == "gzip"):
self._decompressor = GzipDecompressor() self._decompressor = GzipDecompressor()
if self.headers.get("Transfer-Encoding") == "chunked": if self.headers.get("Transfer-Encoding") == "chunked":
self.chunks = [] self.chunks = []
self.stream.read_until(b("\r\n"), self._on_chunk_length) self.stream.read_until(b"\r\n", self._on_chunk_length)
elif content_length is not None: elif content_length is not None:
self.stream.read_bytes(content_length, self._on_body) self.stream.read_bytes(content_length, self._on_body)
else: else:
self.stream.read_until_close(self._on_body) self.stream.read_until_close(self._on_body)
def _on_body(self, data): def _on_body(self, data):
if self._timeout is not None: self._remove_timeout()
self.io_loop.remove_timeout(self._timeout)
self._timeout = None
original_request = getattr(self.request, "original_request", original_request = getattr(self.request, "original_request",
self.request) self.request)
if (self.request.follow_redirects and if (self.request.follow_redirects and
self.request.max_redirects > 0 and self.request.max_redirects > 0 and
self.code in (301, 302, 303, 307)): self.code in (301, 302, 303, 307)):
assert isinstance(self.request, _RequestProxy) assert isinstance(self.request, _RequestProxy)
new_request = copy.copy(self.request.request) new_request = copy.copy(self.request.request)
new_request.url = urlparse.urljoin(self.request.url, new_request.url = urlparse.urljoin(self.request.url,
@@ -472,13 +455,13 @@ class _HTTPConnection(object):
# all the data has been decompressed, so we don't need to # all the data has been decompressed, so we don't need to
# decompress again in _on_body # decompress again in _on_body
self._decompressor = None self._decompressor = None
self._on_body(b('').join(self.chunks)) self._on_body(b''.join(self.chunks))
else: else:
self.stream.read_bytes(length + 2, # chunk ends with \r\n self.stream.read_bytes(length + 2, # chunk ends with \r\n
self._on_chunk_data) self._on_chunk_data)
def _on_chunk_data(self, data): def _on_chunk_data(self, data):
assert data[-2:] == b("\r\n") assert data[-2:] == b"\r\n"
chunk = data[:-2] chunk = data[:-2]
if self._decompressor: if self._decompressor:
chunk = self._decompressor.decompress(chunk) chunk = self._decompressor.decompress(chunk)
@@ -486,69 +469,9 @@ class _HTTPConnection(object):
self.request.streaming_callback(chunk) self.request.streaming_callback(chunk)
else: else:
self.chunks.append(chunk) self.chunks.append(chunk)
self.stream.read_until(b("\r\n"), self._on_chunk_length) self.stream.read_until(b"\r\n", self._on_chunk_length)
# match_hostname was added to the standard library ssl module in python 3.2.
# The following code was backported for older releases and copied from
# https://bitbucket.org/brandon/backports.ssl_match_hostname
class CertificateError(ValueError):
pass
def _dnsname_to_pat(dn):
pats = []
for frag in dn.split(r'.'):
if frag == '*':
# When '*' is a fragment by itself, it matches a non-empty dotless
# fragment.
pats.append('[^.]+')
else:
# Otherwise, '*' matches any dotless fragment.
frag = re.escape(frag)
pats.append(frag.replace(r'\*', '[^.]*'))
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
are mostly followed, but IP addresses are not accepted for *hostname*.
CertificateError is raised on failure. On success, the function
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if not san:
# The subject is only checked when subjectAltName is empty
for sub in cert.get('subject', ()):
for key, value in sub:
# XXX according to RFC 2818, the most specific Common Name
# must be used.
if key == 'commonName':
if _dnsname_to_pat(value).match(hostname):
return
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")
if __name__ == "__main__": if __name__ == "__main__":
AsyncHTTPClient.configure(SimpleAsyncHTTPClient) AsyncHTTPClient.configure(SimpleAsyncHTTPClient)
main() main()
+41 -17
View File
@@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
'''StackContext allows applications to maintain threadlocal-like state """StackContext allows applications to maintain threadlocal-like state
that follows execution as it moves to other execution contexts. that follows execution as it moves to other execution contexts.
The motivating examples are to eliminate the need for explicit The motivating examples are to eliminate the need for explicit
@@ -64,9 +64,9 @@ Here are a few rules of thumb for when it's necessary:
persist across asynchronous calls, create a new `StackContext` (or persist across asynchronous calls, create a new `StackContext` (or
`ExceptionStackContext`), and make your asynchronous calls in a ``with`` `ExceptionStackContext`), and make your asynchronous calls in a ``with``
block that references your `StackContext`. block that references your `StackContext`.
''' """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import contextlib import contextlib
import functools import functools
@@ -77,6 +77,10 @@ import threading
from tornado.util import raise_exc_info from tornado.util import raise_exc_info
class StackContextInconsistentError(Exception):
pass
class _State(threading.local): class _State(threading.local):
def __init__(self): def __init__(self):
self.contexts = () self.contexts = ()
@@ -84,7 +88,7 @@ _state = _State()
class StackContext(object): class StackContext(object):
'''Establishes the given context as a StackContext that will be transferred. """Establishes the given context as a StackContext that will be transferred.
Note that the parameter is a callable that returns a context Note that the parameter is a callable that returns a context
manager, not the context itself. That is, where for a manager, not the context itself. That is, where for a
@@ -102,7 +106,7 @@ class StackContext(object):
deactivating a context does not affect any instances of that deactivating a context does not affect any instances of that
context that are currently pending). This is an advanced feature context that are currently pending). This is an advanced feature
and not necessary in most applications. and not necessary in most applications.
''' """
def __init__(self, context_factory, _active_cell=None): def __init__(self, context_factory, _active_cell=None):
self.context_factory = context_factory self.context_factory = context_factory
self.active_cell = _active_cell or [True] self.active_cell = _active_cell or [True]
@@ -113,8 +117,10 @@ class StackContext(object):
def __enter__(self): def __enter__(self):
self.old_contexts = _state.contexts self.old_contexts = _state.contexts
# _state.contexts is a tuple of (class, arg, active_cell) tuples # _state.contexts is a tuple of (class, arg, active_cell) tuples
_state.contexts = (self.old_contexts + self.new_contexts = (self.old_contexts +
((StackContext, self.context_factory, self.active_cell),)) ((StackContext, self.context_factory,
self.active_cell),))
_state.contexts = self.new_contexts
try: try:
self.context = self.context_factory() self.context = self.context_factory()
self.context.__enter__() self.context.__enter__()
@@ -127,11 +133,23 @@ class StackContext(object):
try: try:
return self.context.__exit__(type, value, traceback) return self.context.__exit__(type, value, traceback)
finally: finally:
final_contexts = _state.contexts
_state.contexts = self.old_contexts _state.contexts = self.old_contexts
# Generator coroutines and with-statements with non-local
# effects interact badly. Check here for signs of
# the stack getting out of sync.
# Note that this check comes after restoring _state.context
# so that if it fails things are left in a (relatively)
# consistent state.
if final_contexts is not self.new_contexts:
raise StackContextInconsistentError(
'stack_context inconsistency (may be caused by yield '
'within a "with StackContext" block)')
self.old_contexts = self.new_contexts = None
class ExceptionStackContext(object): class ExceptionStackContext(object):
'''Specialization of StackContext for exception handling. """Specialization of StackContext for exception handling.
The supplied exception_handler function will be called in the The supplied exception_handler function will be called in the
event of an uncaught exception in this context. The semantics are event of an uncaught exception in this context. The semantics are
@@ -142,16 +160,17 @@ class ExceptionStackContext(object):
If the exception handler returns true, the exception will be If the exception handler returns true, the exception will be
consumed and will not be propagated to other exception handlers. consumed and will not be propagated to other exception handlers.
''' """
def __init__(self, exception_handler, _active_cell=None): def __init__(self, exception_handler, _active_cell=None):
self.exception_handler = exception_handler self.exception_handler = exception_handler
self.active_cell = _active_cell or [True] self.active_cell = _active_cell or [True]
def __enter__(self): def __enter__(self):
self.old_contexts = _state.contexts self.old_contexts = _state.contexts
_state.contexts = (self.old_contexts + self.new_contexts = (self.old_contexts +
((ExceptionStackContext, self.exception_handler, ((ExceptionStackContext, self.exception_handler,
self.active_cell),)) self.active_cell),))
_state.contexts = self.new_contexts
return lambda: operator.setitem(self.active_cell, 0, False) return lambda: operator.setitem(self.active_cell, 0, False)
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
@@ -159,17 +178,22 @@ class ExceptionStackContext(object):
if type is not None: if type is not None:
return self.exception_handler(type, value, traceback) return self.exception_handler(type, value, traceback)
finally: finally:
final_contexts = _state.contexts
_state.contexts = self.old_contexts _state.contexts = self.old_contexts
self.old_contexts = None if final_contexts is not self.new_contexts:
raise StackContextInconsistentError(
'stack_context inconsistency (may be caused by yield '
'within a "with StackContext" block)')
self.old_contexts = self.new_contexts = None
class NullContext(object): class NullContext(object):
'''Resets the StackContext. """Resets the StackContext.
Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient) Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
where the stack that caused the creating is not relevant to future where the stack that caused the creating is not relevant to future
operations. operations.
''' """
def __enter__(self): def __enter__(self):
self.old_contexts = _state.contexts self.old_contexts = _state.contexts
_state.contexts = () _state.contexts = ()
@@ -183,13 +207,13 @@ class _StackContextWrapper(functools.partial):
def wrap(fn): def wrap(fn):
'''Returns a callable object that will restore the current StackContext """Returns a callable object that will restore the current StackContext
when executed. when executed.
Use this whenever saving a callback to be executed later in a Use this whenever saving a callback to be executed later in a
different execution context (either in a different thread or different execution context (either in a different thread or
asynchronously in the same thread). asynchronously in the same thread).
''' """
if fn is None or fn.__class__ is _StackContextWrapper: if fn is None or fn.__class__ is _StackContextWrapper:
return fn return fn
# functools.wraps doesn't appear to work on functools.partial objects # functools.wraps doesn't appear to work on functools.partial objects
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python
#
# Copyright 2011 Facebook
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""A non-blocking, single-threaded TCP server."""
from __future__ import absolute_import, division, print_function, with_statement
import errno
import os
import socket
import ssl
from tornado.log import app_log
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream
from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket
from tornado import process
class TCPServer(object):
r"""A non-blocking, single-threaded TCP server.
To use `TCPServer`, define a subclass which overrides the `handle_stream`
method.
`TCPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
To make this server serve SSL traffic, send the ssl_options dictionary
argument with the arguments required for the `ssl.wrap_socket` method,
including "certfile" and "keyfile"::
TCPServer(ssl_options={
"certfile": os.path.join(data_dir, "mydomain.crt"),
"keyfile": os.path.join(data_dir, "mydomain.key"),
})
`TCPServer` initialization follows one of three patterns:
1. `listen`: simple single-process::
server = TCPServer()
server.listen(8888)
IOLoop.instance().start()
2. `bind`/`start`: simple multi-process::
server = TCPServer()
server.bind(8888)
server.start(0) # Forks multiple sub-processes
IOLoop.instance().start()
When using this interface, an `IOLoop` must *not* be passed
to the `TCPServer` constructor. `start` will always start
the server on the default singleton `IOLoop`.
3. `add_sockets`: advanced multi-process::
sockets = bind_sockets(8888)
tornado.process.fork_processes(0)
server = TCPServer()
server.add_sockets(sockets)
IOLoop.instance().start()
The `add_sockets` interface is more complicated, but it can be
used with `tornado.process.fork_processes` to give you more
flexibility in when the fork happens. `add_sockets` can
also be used in single-process servers if you want to create
your listening sockets in some way other than
`bind_sockets`.
"""
def __init__(self, io_loop=None, ssl_options=None):
self.io_loop = io_loop
self.ssl_options = ssl_options
self._sockets = {} # fd -> socket object
self._pending_sockets = []
self._started = False
# Verify the SSL options. Otherwise we don't get errors until clients
# connect. This doesn't verify that the keys are legitimate, but
# the SSL module doesn't do that until there is a connected socket
# which seems like too much work
if self.ssl_options is not None and isinstance(self.ssl_options, dict):
# Only certfile is required: it can contain both keys
if 'certfile' not in self.ssl_options:
raise KeyError('missing key "certfile" in ssl_options')
if not os.path.exists(self.ssl_options['certfile']):
raise ValueError('certfile "%s" does not exist' %
self.ssl_options['certfile'])
if ('keyfile' in self.ssl_options and
not os.path.exists(self.ssl_options['keyfile'])):
raise ValueError('keyfile "%s" does not exist' %
self.ssl_options['keyfile'])
def listen(self, port, address=""):
"""Starts accepting connections on the given port.
This method may be called more than once to listen on multiple ports.
`listen` takes effect immediately; it is not necessary to call
`TCPServer.start` afterwards. It is, however, necessary to start
the `IOLoop`.
"""
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets)
def add_sockets(self, sockets):
"""Makes this server start accepting connections on the given sockets.
The ``sockets`` parameter is a list of socket objects such as
those returned by `bind_sockets`.
`add_sockets` is typically used in combination with that
method and `tornado.process.fork_processes` to provide greater
control over the initialization of a multi-process server.
"""
if self.io_loop is None:
self.io_loop = IOLoop.instance()
for sock in sockets:
self._sockets[sock.fileno()] = sock
add_accept_handler(sock, self._handle_connection,
io_loop=self.io_loop)
def add_socket(self, socket):
"""Singular version of `add_sockets`. Takes a single socket object."""
self.add_sockets([socket])
def bind(self, port, address=None, family=socket.AF_UNSPEC, backlog=128):
"""Binds this server to the given port on the given address.
To start the server, call `start`. If you want to run this server
in a single process, you can call `listen` as a shortcut to the
sequence of `bind` and `start` calls.
Address may be either an IP address or hostname. If it's a hostname,
the server will listen on all IP addresses associated with the
name. Address may be an empty string or None to listen on all
available interfaces. Family may be set to either ``socket.AF_INET``
or ``socket.AF_INET6`` to restrict to ipv4 or ipv6 addresses, otherwise
both will be used if available.
The ``backlog`` argument has the same meaning as for
`socket.listen`.
This method may be called multiple times prior to `start` to listen
on multiple ports or interfaces.
"""
sockets = bind_sockets(port, address=address, family=family,
backlog=backlog)
if self._started:
self.add_sockets(sockets)
else:
self._pending_sockets.extend(sockets)
def start(self, num_processes=1):
"""Starts this server in the IOLoop.
By default, we run the server in this process and do not fork any
additional child process.
If num_processes is ``None`` or <= 0, we detect the number of cores
available on this machine and fork that number of child
processes. If num_processes is given and > 1, we fork that
specific number of sub-processes.
Since we use processes and not threads, there is no shared memory
between any server code.
Note that multiple processes are not compatible with the autoreload
module (or the ``debug=True`` option to `tornado.web.Application`).
When using multiple processes, no IOLoops can be created or
referenced until after the call to ``TCPServer.start(n)``.
"""
assert not self._started
self._started = True
if num_processes != 1:
process.fork_processes(num_processes)
sockets = self._pending_sockets
self._pending_sockets = []
self.add_sockets(sockets)
def stop(self):
"""Stops listening for new connections.
Requests currently in progress may still continue after the
server is stopped.
"""
for fd, sock in self._sockets.items():
self.io_loop.remove_handler(fd)
sock.close()
def handle_stream(self, stream, address):
"""Override to handle a new `IOStream` from an incoming connection."""
raise NotImplementedError()
def _handle_connection(self, connection, address):
if self.ssl_options is not None:
assert ssl, "Python 2.6+ and OpenSSL required for SSL"
try:
connection = ssl_wrap_socket(connection,
self.ssl_options,
server_side=True,
do_handshake_on_connect=False)
except ssl.SSLError as err:
if err.args[0] == ssl.SSL_ERROR_EOF:
return connection.close()
else:
raise
except socket.error as err:
if err.args[0] == errno.ECONNABORTED:
return connection.close()
else:
raise
try:
if self.ssl_options is not None:
stream = SSLIOStream(connection, io_loop=self.io_loop)
else:
stream = IOStream(connection, io_loop=self.io_loop)
self.handle_stream(stream, address)
except Exception:
app_log.error("Error in connection callback", exc_info=True)
+20 -14
View File
@@ -179,9 +179,8 @@ with ``{# ... #}``.
``{% continue %}`` may be used inside the loop. ``{% continue %}`` may be used inside the loop.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import cStringIO
import datetime import datetime
import linecache import linecache
import os.path import os.path
@@ -191,7 +190,12 @@ import threading
from tornado import escape from tornado import escape
from tornado.log import app_log from tornado.log import app_log
from tornado.util import bytes_type, ObjectDict from tornado.util import bytes_type, ObjectDict, exec_in, unicode_type
try:
from cStringIO import StringIO # py2
except ImportError:
from io import StringIO # py3
_DEFAULT_AUTOESCAPE = "xhtml_escape" _DEFAULT_AUTOESCAPE = "xhtml_escape"
_UNSET = object() _UNSET = object()
@@ -226,10 +230,12 @@ class Template(object):
try: try:
# Under python2.5, the fake filename used here must match # Under python2.5, the fake filename used here must match
# the module name used in __name__ below. # the module name used in __name__ below.
# The dont_inherit flag prevents template.py's future imports
# from being applied to the generated code.
self.compiled = compile( self.compiled = compile(
escape.to_unicode(self.code), escape.to_unicode(self.code),
"%s.generated.py" % self.name.replace('.', '_'), "%s.generated.py" % self.name.replace('.', '_'),
"exec") "exec", dont_inherit=True)
except Exception: except Exception:
formatted_code = _format_code(self.code).rstrip() formatted_code = _format_code(self.code).rstrip()
app_log.error("%s code:\n%s", self.name, formatted_code) app_log.error("%s code:\n%s", self.name, formatted_code)
@@ -246,7 +252,7 @@ class Template(object):
"linkify": escape.linkify, "linkify": escape.linkify,
"datetime": datetime, "datetime": datetime,
"_utf8": escape.utf8, # for internal use "_utf8": escape.utf8, # for internal use
"_string_types": (unicode, bytes_type), "_string_types": (unicode_type, bytes_type),
# __name__ and __loader__ allow the traceback mechanism to find # __name__ and __loader__ allow the traceback mechanism to find
# the generated source code. # the generated source code.
"__name__": self.name.replace('.', '_'), "__name__": self.name.replace('.', '_'),
@@ -254,7 +260,7 @@ class Template(object):
} }
namespace.update(self.namespace) namespace.update(self.namespace)
namespace.update(kwargs) namespace.update(kwargs)
exec self.compiled in namespace exec_in(self.compiled, namespace)
execute = namespace["_execute"] execute = namespace["_execute"]
# Clear the traceback module's cache of source data now that # Clear the traceback module's cache of source data now that
# we've generated a new template (mainly for this module's # we've generated a new template (mainly for this module's
@@ -263,7 +269,7 @@ class Template(object):
return execute() return execute()
def _generate_python(self, loader, compress_whitespace): def _generate_python(self, loader, compress_whitespace):
buffer = cStringIO.StringIO() buffer = StringIO()
try: try:
# named_blocks maps from names to _NamedBlock objects # named_blocks maps from names to _NamedBlock objects
named_blocks = {} named_blocks = {}
@@ -346,8 +352,8 @@ class Loader(BaseLoader):
def resolve_path(self, name, parent_path=None): def resolve_path(self, name, parent_path=None):
if parent_path and not parent_path.startswith("<") and \ if parent_path and not parent_path.startswith("<") and \
not parent_path.startswith("/") and \ not parent_path.startswith("/") and \
not name.startswith("/"): not name.startswith("/"):
current_path = os.path.join(self.root, parent_path) current_path = os.path.join(self.root, parent_path)
file_dir = os.path.dirname(os.path.abspath(current_path)) file_dir = os.path.dirname(os.path.abspath(current_path))
relative_path = os.path.abspath(os.path.join(file_dir, name)) relative_path = os.path.abspath(os.path.join(file_dir, name))
@@ -371,8 +377,8 @@ class DictLoader(BaseLoader):
def resolve_path(self, name, parent_path=None): def resolve_path(self, name, parent_path=None):
if parent_path and not parent_path.startswith("<") and \ if parent_path and not parent_path.startswith("<") and \
not parent_path.startswith("/") and \ not parent_path.startswith("/") and \
not name.startswith("/"): not name.startswith("/"):
file_dir = posixpath.dirname(parent_path) file_dir = posixpath.dirname(parent_path)
name = posixpath.normpath(posixpath.join(file_dir, name)) name = posixpath.normpath(posixpath.join(file_dir, name))
return name return name
@@ -620,7 +626,7 @@ class _CodeWriter(object):
ancestors = ["%s:%d" % (tmpl.name, lineno) ancestors = ["%s:%d" % (tmpl.name, lineno)
for (tmpl, lineno) in self.include_stack] for (tmpl, lineno) in self.include_stack]
line_comment += ' (via %s)' % ', '.join(reversed(ancestors)) line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
print >> self.file, " " * indent + line + line_comment print(" " * indent + line + line_comment, file=self.file)
class _TemplateReader(object): class _TemplateReader(object):
@@ -708,7 +714,7 @@ def _parse(reader, template, in_block=None, in_loop=None):
# innermost ones. This is useful when generating languages # innermost ones. This is useful when generating languages
# like latex where curlies are also meaningful # like latex where curlies are also meaningful
if (curly + 2 < reader.remaining() and if (curly + 2 < reader.remaining() and
reader[curly + 1] == '{' and reader[curly + 2] == '{'): reader[curly + 1] == '{' and reader[curly + 2] == '{'):
curly += 1 curly += 1
continue continue
break break
@@ -775,7 +781,7 @@ def _parse(reader, template, in_block=None, in_loop=None):
if allowed_parents is not None: if allowed_parents is not None:
if not in_block: if not in_block:
raise ParseError("%s outside %s block" % raise ParseError("%s outside %s block" %
(operator, allowed_parents)) (operator, allowed_parents))
if in_block not in allowed_parents: if in_block not in allowed_parents:
raise ParseError("%s block cannot be attached to %s block" % (operator, in_block)) raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
body.chunks.append(_IntermediateControlBlock(contents, line)) body.chunks.append(_IntermediateControlBlock(contents, line))
+69 -31
View File
@@ -18,10 +18,10 @@ inheritance. See the docstrings for each class/function below for more
information. information.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
from cStringIO import StringIO
try: try:
from tornado import gen
from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import AsyncHTTPClient
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.simple_httpclient import SimpleAsyncHTTPClient from tornado.simple_httpclient import SimpleAsyncHTTPClient
@@ -31,20 +31,27 @@ except ImportError:
# These modules are not importable on app engine. Parts of this module # These modules are not importable on app engine. Parts of this module
# won't work, but e.g. LogTrapTestCase and main() will. # won't work, but e.g. LogTrapTestCase and main() will.
AsyncHTTPClient = None AsyncHTTPClient = None
gen = None
HTTPServer = None HTTPServer = None
IOLoop = None IOLoop = None
netutil = None netutil = None
SimpleAsyncHTTPClient = None SimpleAsyncHTTPClient = None
from tornado.log import gen_log from tornado.log import gen_log
from tornado.stack_context import ExceptionStackContext from tornado.stack_context import ExceptionStackContext
from tornado.util import raise_exc_info from tornado.util import raise_exc_info, basestring_type
import functools
import logging import logging
import os import os
import re import re
import signal import signal
import socket import socket
import sys import sys
import time import types
try:
from io import StringIO # py3
except ImportError:
from cStringIO import StringIO # py2
# Tornado's own test suite requires the updated unittest module # Tornado's own test suite requires the updated unittest module
# (either py27+ or unittest2) so tornado.test.util enforces # (either py27+ or unittest2) so tornado.test.util enforces
@@ -151,7 +158,7 @@ class AsyncTestCase(unittest.TestCase):
def tearDown(self): def tearDown(self):
self.io_loop.clear_current() self.io_loop.clear_current()
if (not IOLoop.initialized() or if (not IOLoop.initialized() or
self.io_loop is not IOLoop.instance()): self.io_loop is not IOLoop.instance()):
# Try to clean up any file descriptors left open in the ioloop. # Try to clean up any file descriptors left open in the ioloop.
# This avoids leaks, especially when tests are run repeatedly # This avoids leaks, especially when tests are run repeatedly
# in the same process with autoreload (because curl does not # in the same process with autoreload (because curl does not
@@ -160,10 +167,10 @@ class AsyncTestCase(unittest.TestCase):
super(AsyncTestCase, self).tearDown() super(AsyncTestCase, self).tearDown()
def get_new_ioloop(self): def get_new_ioloop(self):
'''Creates a new IOLoop for this test. May be overridden in """Creates a new IOLoop for this test. May be overridden in
subclasses for tests that require a specific IOLoop (usually subclasses for tests that require a specific IOLoop (usually
the singleton). the singleton).
''' """
return IOLoop() return IOLoop()
def _handle_exception(self, typ, value, tb): def _handle_exception(self, typ, value, tb):
@@ -185,12 +192,12 @@ class AsyncTestCase(unittest.TestCase):
self.__rethrow() self.__rethrow()
def stop(self, _arg=None, **kwargs): def stop(self, _arg=None, **kwargs):
'''Stops the ioloop, causing one pending (or future) call to wait() """Stops the ioloop, causing one pending (or future) call to wait()
to return. to return.
Keyword arguments or a single positional argument passed to stop() are Keyword arguments or a single positional argument passed to stop() are
saved and will be returned by wait(). saved and will be returned by wait().
''' """
assert _arg is None or not kwargs assert _arg is None or not kwargs
self.__stop_args = kwargs or _arg self.__stop_args = kwargs or _arg
if self.__running: if self.__running:
@@ -211,20 +218,21 @@ class AsyncTestCase(unittest.TestCase):
def timeout_func(): def timeout_func():
try: try:
raise self.failureException( raise self.failureException(
'Async operation timed out after %s seconds' % 'Async operation timed out after %s seconds' %
timeout) timeout)
except Exception: except Exception:
self.__failure = sys.exc_info() self.__failure = sys.exc_info()
self.stop() self.stop()
if self.__timeout is not None:
self.io_loop.remove_timeout(self.__timeout)
self.__timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, timeout_func) self.__timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, timeout_func)
while True: while True:
self.__running = True self.__running = True
self.io_loop.start() self.io_loop.start()
if (self.__failure is not None or if (self.__failure is not None or
condition is None or condition()): condition is None or condition()):
break break
if self.__timeout is not None:
self.io_loop.remove_timeout(self.__timeout)
self.__timeout = None
assert self.__stopped assert self.__stopped
self.__stopped = False self.__stopped = False
self.__rethrow() self.__rethrow()
@@ -234,7 +242,7 @@ class AsyncTestCase(unittest.TestCase):
class AsyncHTTPTestCase(AsyncTestCase): class AsyncHTTPTestCase(AsyncTestCase):
'''A test case that starts up an HTTP server. """A test case that starts up an HTTP server.
Subclasses must override get_app(), which returns the Subclasses must override get_app(), which returns the
tornado.web.Application (or other HTTPServer callback) to be tested. tornado.web.Application (or other HTTPServer callback) to be tested.
@@ -255,7 +263,7 @@ class AsyncHTTPTestCase(AsyncTestCase):
self.http_client.fetch(self.get_url('/'), self.stop) self.http_client.fetch(self.get_url('/'), self.stop)
response = self.wait() response = self.wait()
# test contents of response # test contents of response
''' """
def setUp(self): def setUp(self):
super(AsyncHTTPTestCase, self).setUp() super(AsyncHTTPTestCase, self).setUp()
sock, port = bind_unused_port() sock, port = bind_unused_port()
@@ -314,7 +322,7 @@ class AsyncHTTPTestCase(AsyncTestCase):
def tearDown(self): def tearDown(self):
self.http_server.stop() self.http_server.stop()
if (not IOLoop.initialized() or if (not IOLoop.initialized() or
self.http_client.io_loop is not IOLoop.instance()): self.http_client.io_loop is not IOLoop.instance()):
self.http_client.close() self.http_client.close()
super(AsyncHTTPTestCase, self).tearDown() super(AsyncHTTPTestCase, self).tearDown()
@@ -342,13 +350,43 @@ class AsyncHTTPSTestCase(AsyncHTTPTestCase):
# openssl req -new -keyout tornado/test/test.key -out tornado/test/test.crt -nodes -days 3650 -x509 # openssl req -new -keyout tornado/test/test.key -out tornado/test/test.crt -nodes -days 3650 -x509
module_dir = os.path.dirname(__file__) module_dir = os.path.dirname(__file__)
return dict( return dict(
certfile=os.path.join(module_dir, 'test', 'test.crt'), certfile=os.path.join(module_dir, 'test', 'test.crt'),
keyfile=os.path.join(module_dir, 'test', 'test.key')) keyfile=os.path.join(module_dir, 'test', 'test.key'))
def get_protocol(self): def get_protocol(self):
return 'https' return 'https'
def gen_test(f):
"""Testing equivalent of ``@gen.engine``, to be applied to test methods.
``@gen.engine`` cannot be used on tests because the `IOLoop` is not
already running. ``@gen_test`` should be applied to test methods
on subclasses of `AsyncTestCase`.
Note that unlike most uses of ``@gen.engine``, ``@gen_test`` can
detect automatically when the function finishes cleanly so there
is no need to run a callback to signal completion.
Example::
class MyTest(AsyncHTTPTestCase):
@gen_test
def test_something(self):
response = yield gen.Task(self.fetch('/'))
"""
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
result = f(self, *args, **kwargs)
if result is None:
return
assert isinstance(result, types.GeneratorType)
runner = gen.Runner(result, self.stop)
runner.run()
self.wait()
return wrapper
class LogTrapTestCase(unittest.TestCase): class LogTrapTestCase(unittest.TestCase):
"""A test case that captures and discards all logging output """A test case that captures and discards all logging output
if the test passes. if the test passes.
@@ -361,21 +399,21 @@ class LogTrapTestCase(unittest.TestCase):
This class assumes that only one log handler is configured and that This class assumes that only one log handler is configured and that
it is a StreamHandler. This is true for both logging.basicConfig it is a StreamHandler. This is true for both logging.basicConfig
and the "pretty logging" configured by tornado.options. and the "pretty logging" configured by tornado.options. It is not
compatible with other log buffering mechanisms, such as those provided
by some test runners.
""" """
def run(self, result=None): def run(self, result=None):
logger = logging.getLogger() logger = logging.getLogger()
if len(logger.handlers) > 1:
# Multiple handlers have been defined. It gets messy to handle
# this, especially since the handlers may have different
# formatters. Just leave the logging alone in this case.
super(LogTrapTestCase, self).run(result)
return
if not logger.handlers: if not logger.handlers:
logging.basicConfig() logging.basicConfig()
self.assertEqual(len(logger.handlers), 1)
handler = logger.handlers[0] handler = logger.handlers[0]
assert isinstance(handler, logging.StreamHandler) if (len(logger.handlers) > 1 or
not isinstance(handler, logging.StreamHandler)):
# Logging has been configured in a way we don't recognize,
# so just leave it alone.
super(LogTrapTestCase, self).run(result)
return
old_stream = handler.stream old_stream = handler.stream
try: try:
handler.stream = StringIO() handler.stream = StringIO()
@@ -410,7 +448,7 @@ class ExpectLog(logging.Filter):
:param required: If true, an exeption will be raised if the end of :param required: If true, an exeption will be raised if the end of
the ``with`` statement is reached without matching any log entries. the ``with`` statement is reached without matching any log entries.
""" """
if isinstance(logger, basestring): if isinstance(logger, basestring_type):
logger = logging.getLogger(logger) logger = logging.getLogger(logger)
self.logger = logger self.logger = logger
self.regex = re.compile(regex) self.regex = re.compile(regex)
@@ -496,7 +534,7 @@ def main(**kwargs):
kwargs['buffer'] = True kwargs['buffer'] = True
if __name__ == '__main__' and len(argv) == 1: if __name__ == '__main__' and len(argv) == 1:
print >> sys.stderr, "No tests specified" print("No tests specified", file=sys.stderr)
sys.exit(1) sys.exit(1)
try: try:
# In order to be able to run tests by their fully-qualified name # In order to be able to run tests by their fully-qualified name
@@ -509,7 +547,7 @@ def main(**kwargs):
unittest.main(module=None, argv=argv, **kwargs) unittest.main(module=None, argv=argv, **kwargs)
else: else:
unittest.main(defaultTest="all", argv=argv, **kwargs) unittest.main(defaultTest="all", argv=argv, **kwargs)
except SystemExit, e: except SystemExit as e:
if e.code == 0: if e.code == 0:
gen_log.info('PASS') gen_log.info('PASS')
else: else:
+78 -29
View File
@@ -1,7 +1,9 @@
"""Miscellaneous utility functions.""" """Miscellaneous utility functions."""
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import inspect
import sys
import zlib import zlib
@@ -62,38 +64,47 @@ def import_object(name):
obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0) obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
return getattr(obj, parts[-1]) return getattr(obj, parts[-1])
# Fake byte literal support: In python 2.6+, you can say b"foo" to get # Fake unicode literal support: Python 3.2 doesn't have the u'' marker for
# a byte literal (str in 2.x, bytes in 3.x). There's no way to do this # literal strings, and alternative solutions like "from __future__ import
# in a way that supports 2.5, though, so we need a function wrapper # unicode_literals" have other problems (see PEP 414). u() can be applied
# to convert our string literals. b() should only be applied to literal # to ascii strings that include \u escapes (but they must not contain
# latin1 strings. Once we drop support for 2.5, we can remove this function # literal non-ascii characters).
# and just use byte literals. if type('') is not type(b''):
if str is unicode: def u(s):
def b(s):
return s.encode('latin1')
bytes_type = bytes
else:
def b(s):
return s return s
bytes_type = bytes
unicode_type = str
basestring_type = str
else:
def u(s):
return s.decode('unicode_escape')
bytes_type = str bytes_type = str
unicode_type = unicode
basestring_type = basestring
if sys.version_info > (3,):
exec("""
def raise_exc_info(exc_info): def raise_exc_info(exc_info):
"""Re-raise an exception (with original traceback) from an exc_info tuple. raise exc_info[1].with_traceback(exc_info[2])
The argument is a ``(type, value, traceback)`` tuple as returned by def exec_in(code, glob, loc=None):
`sys.exc_info`. if isinstance(code, str):
""" code = compile(code, '<string>', 'exec', dont_inherit=True)
# 2to3 isn't smart enough to convert three-argument raise exec(code, glob, loc)
# statements correctly in some cases. """)
if isinstance(exc_info[1], exc_info[0]): else:
raise exc_info[1], None, exc_info[2] exec("""
# After 2to3: raise exc_info[1].with_traceback(exc_info[2]) def raise_exc_info(exc_info):
else: raise exc_info[0], exc_info[1], exc_info[2]
# I think this branch is only taken for string exceptions,
# which were removed in Python 2.6. def exec_in(code, glob, loc=None):
raise exc_info[0], exc_info[1], exc_info[2] if isinstance(code, basestring):
# After 2to3: raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) # exec(string) inherits the caller's future imports; compile
# the string first to prevent that.
code = compile(code, '<string>', 'exec', dont_inherit=True)
exec code in glob, loc
""")
class Configurable(object): class Configurable(object):
@@ -164,7 +175,7 @@ class Configurable(object):
some parameters. some parameters.
""" """
base = cls.configurable_base() base = cls.configurable_base()
if isinstance(impl, (unicode, bytes_type)): if isinstance(impl, (unicode_type, bytes_type)):
impl = import_object(impl) impl = import_object(impl)
if impl is not None and not issubclass(impl, cls): if impl is not None and not issubclass(impl, cls):
raise ValueError("Invalid subclass of %s" % cls) raise ValueError("Invalid subclass of %s" % cls)
@@ -179,7 +190,6 @@ class Configurable(object):
base.__impl_class = cls.configurable_default() base.__impl_class = cls.configurable_default()
return base.__impl_class return base.__impl_class
@classmethod @classmethod
def _save_configuration(cls): def _save_configuration(cls):
base = cls.configurable_base() base = cls.configurable_base()
@@ -192,6 +202,45 @@ class Configurable(object):
base.__impl_kwargs = saved[1] base.__impl_kwargs = saved[1]
class ArgReplacer(object):
"""Replaces one value in an ``args, kwargs`` pair.
Inspects the function signature to find an argument by name
whether it is passed by position or keyword. For use in decorators
and similar wrappers.
"""
def __init__(self, func, name):
"""Create an ArgReplacer for the named argument to the given function.
"""
self.name = name
try:
self.arg_pos = inspect.getargspec(func).args.index(self.name)
except ValueError:
# Not a positional parameter
self.arg_pos = None
def replace(self, new_value, args, kwargs):
"""Replace the named argument in ``args, kwargs`` with ``new_value``.
Returns ``(old_value, args, kwargs)``. The returned ``args`` and
``kwargs`` objects may not be the same as the input objects, or
the input objects may be mutated.
If the named argument was not found, ``new_value`` will be added
to ``kwargs`` and None will be returned as ``old_value``.
"""
if self.arg_pos is not None and len(args) > self.arg_pos:
# The arg to replace is passed positionally
old_value = args[self.arg_pos]
args = list(args) # *args is normally a tuple
args[self.arg_pos] = new_value
else:
# The arg to replace is either omitted or passed by keyword.
old_value = kwargs.get(self.name)
kwargs[self.name] = new_value
return old_value, args, kwargs
def doctests(): def doctests():
import doctest import doctest
return doctest.DocTestSuite() return doctest.DocTestSuite()
+101 -98
View File
@@ -49,21 +49,18 @@ threads it is important to use IOLoop.add_callback to transfer control
back to the main thread before finishing the request. back to the main thread before finishing the request.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import Cookie
import base64 import base64
import binascii import binascii
import calendar
import datetime import datetime
import email.utils import email.utils
import functools import functools
import gzip import gzip
import hashlib import hashlib
import hmac import hmac
import httplib
import itertools
import mimetypes import mimetypes
import numbers
import os.path import os.path
import re import re
import stat import stat
@@ -73,23 +70,37 @@ import time
import tornado import tornado
import traceback import traceback
import types import types
import urllib
import urlparse
import uuid import uuid
from tornado import escape from tornado import escape
from tornado import httputil
from tornado import locale from tornado import locale
from tornado.log import access_log, app_log, gen_log from tornado.log import access_log, app_log, gen_log
from tornado import stack_context from tornado import stack_context
from tornado import template from tornado import template
from tornado.escape import utf8, _unicode from tornado.escape import utf8, _unicode
from tornado.util import b, bytes_type, import_object, ObjectDict, raise_exc_info from tornado.util import bytes_type, import_object, ObjectDict, raise_exc_info, unicode_type
try: try:
from io import BytesIO # python 3 from io import BytesIO # python 3
except ImportError: except ImportError:
from cStringIO import StringIO as BytesIO # python 2 from cStringIO import StringIO as BytesIO # python 2
try:
import Cookie # py2
except ImportError:
import http.cookies as Cookie # py3
try:
import urlparse # py2
except ImportError:
import urllib.parse as urlparse # py3
try:
from urllib import urlencode # py2
except ImportError:
from urllib.parse import urlencode # py3
class RequestHandler(object): class RequestHandler(object):
"""Subclass this class and define get() or post() to make a handler. """Subclass this class and define get() or post() to make a handler.
@@ -103,6 +114,7 @@ class RequestHandler(object):
_template_loaders = {} # {path: template.BaseLoader} _template_loaders = {} # {path: template.BaseLoader}
_template_loader_lock = threading.Lock() _template_loader_lock = threading.Lock()
_remove_control_chars_regex = re.compile(r"[\x00-\x08\x0e-\x1f]")
def __init__(self, application, request, **kwargs): def __init__(self, application, request, **kwargs):
super(RequestHandler, self).__init__() super(RequestHandler, self).__init__()
@@ -116,19 +128,19 @@ class RequestHandler(object):
self.path_args = None self.path_args = None
self.path_kwargs = None self.path_kwargs = None
self.ui = ObjectDict((n, self._ui_method(m)) for n, m in self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
application.ui_methods.iteritems()) application.ui_methods.items())
# UIModules are available as both `modules` and `_modules` in the # UIModules are available as both `modules` and `_modules` in the
# template namespace. Historically only `modules` was available # template namespace. Historically only `modules` was available
# but could be clobbered by user additions to the namespace. # but could be clobbered by user additions to the namespace.
# The template {% module %} directive looks in `_modules` to avoid # The template {% module %} directive looks in `_modules` to avoid
# possible conflicts. # possible conflicts.
self.ui["_modules"] = ObjectDict((n, self._ui_module(n, m)) for n, m in self.ui["_modules"] = ObjectDict((n, self._ui_module(n, m)) for n, m in
application.ui_modules.iteritems()) application.ui_modules.items())
self.ui["modules"] = self.ui["_modules"] self.ui["modules"] = self.ui["_modules"]
self.clear() self.clear()
# Check since connection is not available in WSGI # Check since connection is not available in WSGI
if getattr(self.request, "connection", None): if getattr(self.request, "connection", None):
self.request.connection.stream.set_close_callback( self.request.connection.set_close_callback(
self.on_connection_close) self.on_connection_close)
self.initialize(**kwargs) self.initialize(**kwargs)
@@ -215,25 +227,18 @@ class RequestHandler(object):
def clear(self): def clear(self):
"""Resets all headers and content for this response.""" """Resets all headers and content for this response."""
# The performance cost of tornado.httputil.HTTPHeaders is significant self._headers = httputil.HTTPHeaders({
# (slowing down a benchmark with a trivial handler by more than 10%), "Server": "TornadoServer/%s" % tornado.version,
# and its case-normalization is not generally necessary for "Content-Type": "text/html; charset=UTF-8",
# headers we generate on the server side, so use a plain dict "Date": httputil.format_timestamp(time.gmtime()),
# and list instead. })
self._headers = {
"Server": "TornadoServer/%s" % tornado.version,
"Content-Type": "text/html; charset=UTF-8",
"Date": datetime.datetime.utcnow().strftime(
"%a, %d %b %Y %H:%M:%S GMT"),
}
self._list_headers = []
self.set_default_headers() self.set_default_headers()
if not self.request.supports_http_1_1(): if not self.request.supports_http_1_1():
if self.request.headers.get("Connection") == "Keep-Alive": if self.request.headers.get("Connection") == "Keep-Alive":
self.set_header("Connection", "Keep-Alive") self.set_header("Connection", "Keep-Alive")
self._write_buffer = [] self._write_buffer = []
self._status_code = 200 self._status_code = 200
self._reason = httplib.responses[200] self._reason = httputil.responses[200]
def set_default_headers(self): def set_default_headers(self):
"""Override this to set HTTP headers at the beginning of the request. """Override this to set HTTP headers at the beginning of the request.
@@ -258,7 +263,7 @@ class RequestHandler(object):
self._reason = escape.native_str(reason) self._reason = escape.native_str(reason)
else: else:
try: try:
self._reason = httplib.responses[status_code] self._reason = httputil.responses[status_code]
except KeyError: except KeyError:
raise ValueError("unknown status code %d", status_code) raise ValueError("unknown status code %d", status_code)
@@ -281,7 +286,7 @@ class RequestHandler(object):
Unlike `set_header`, `add_header` may be called multiple times Unlike `set_header`, `add_header` may be called multiple times
to return multiple values for the same header. to return multiple values for the same header.
""" """
self._list_headers.append((name, self._convert_header_value(value))) self._headers.add(name, self._convert_header_value(value))
def clear_header(self, name): def clear_header(self, name):
"""Clears an outgoing header, undoing a previous `set_header` call. """Clears an outgoing header, undoing a previous `set_header` call.
@@ -295,20 +300,19 @@ class RequestHandler(object):
def _convert_header_value(self, value): def _convert_header_value(self, value):
if isinstance(value, bytes_type): if isinstance(value, bytes_type):
pass pass
elif isinstance(value, unicode): elif isinstance(value, unicode_type):
value = value.encode('utf-8') value = value.encode('utf-8')
elif isinstance(value, (int, long)): elif isinstance(value, numbers.Integral):
# return immediately since we know the converted value will be safe # return immediately since we know the converted value will be safe
return str(value) return str(value)
elif isinstance(value, datetime.datetime): elif isinstance(value, datetime.datetime):
t = calendar.timegm(value.utctimetuple()) return httputil.format_timestamp(value)
return email.utils.formatdate(t, localtime=False, usegmt=True)
else: else:
raise TypeError("Unsupported header value %r" % value) raise TypeError("Unsupported header value %r" % value)
# If \n is allowed into the header, it is possible to inject # If \n is allowed into the header, it is possible to inject
# additional headers or split the request. Also cap length to # additional headers or split the request. Also cap length to
# prevent obviously erroneous values. # prevent obviously erroneous values.
if len(value) > 4000 or re.search(b(r"[\x00-\x1f]"), value): if len(value) > 4000 or re.search(br"[\x00-\x1f]", value):
raise ValueError("Unsafe header value %r", value) raise ValueError("Unsafe header value %r", value)
return value return value
@@ -339,13 +343,14 @@ class RequestHandler(object):
The returned values are always unicode. The returned values are always unicode.
""" """
values = [] values = []
for v in self.request.arguments.get(name, []): for v in self.request.arguments.get(name, []):
v = self.decode_argument(v, name=name) v = self.decode_argument(v, name=name)
if isinstance(v, unicode): if isinstance(v, unicode_type):
# Get rid of any weird control chars (unless decoding gave # Get rid of any weird control chars (unless decoding gave
# us bytes, in which case leave it alone) # us bytes, in which case leave it alone)
v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v) v = RequestHandler._remove_control_chars_regex.sub(" ", v)
if strip: if strip:
v = v.strip() v = v.strip()
values.append(v) values.append(v)
@@ -403,12 +408,10 @@ class RequestHandler(object):
expires = datetime.datetime.utcnow() + datetime.timedelta( expires = datetime.datetime.utcnow() + datetime.timedelta(
days=expires_days) days=expires_days)
if expires: if expires:
timestamp = calendar.timegm(expires.utctimetuple()) morsel["expires"] = httputil.format_timestamp(expires)
morsel["expires"] = email.utils.formatdate(
timestamp, localtime=False, usegmt=True)
if path: if path:
morsel["path"] = path morsel["path"] = path
for k, v in kwargs.iteritems(): for k, v in kwargs.items():
if k == 'max_age': if k == 'max_age':
k = 'max-age' k = 'max-age'
morsel[k] = v morsel[k] = v
@@ -421,7 +424,7 @@ class RequestHandler(object):
def clear_all_cookies(self): def clear_all_cookies(self):
"""Deletes all the cookies the user sent with this request.""" """Deletes all the cookies the user sent with this request."""
for name in self.request.cookies.iterkeys(): for name in self.request.cookies:
self.clear_cookie(name) self.clear_cookie(name)
def set_secure_cookie(self, name, value, expires_days=30, **kwargs): def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
@@ -482,7 +485,7 @@ class RequestHandler(object):
assert isinstance(status, int) and 300 <= status <= 399 assert isinstance(status, int) and 300 <= status <= 399
self.set_status(status) self.set_status(status)
# Remove whitespace # Remove whitespace
url = re.sub(b(r"[\x00-\x20]+"), "", utf8(url)) url = re.sub(br"[\x00-\x20]+", "", utf8(url))
self.set_header("Location", urlparse.urljoin(utf8(self.request.uri), self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
url)) url))
self.finish() self.finish()
@@ -523,13 +526,13 @@ class RequestHandler(object):
css_files = [] css_files = []
html_heads = [] html_heads = []
html_bodies = [] html_bodies = []
for module in getattr(self, "_active_modules", {}).itervalues(): for module in getattr(self, "_active_modules", {}).values():
embed_part = module.embedded_javascript() embed_part = module.embedded_javascript()
if embed_part: if embed_part:
js_embed.append(utf8(embed_part)) js_embed.append(utf8(embed_part))
file_part = module.javascript_files() file_part = module.javascript_files()
if file_part: if file_part:
if isinstance(file_part, (unicode, bytes_type)): if isinstance(file_part, (unicode_type, bytes_type)):
js_files.append(file_part) js_files.append(file_part)
else: else:
js_files.extend(file_part) js_files.extend(file_part)
@@ -538,7 +541,7 @@ class RequestHandler(object):
css_embed.append(utf8(embed_part)) css_embed.append(utf8(embed_part))
file_part = module.css_files() file_part = module.css_files()
if file_part: if file_part:
if isinstance(file_part, (unicode, bytes_type)): if isinstance(file_part, (unicode_type, bytes_type)):
css_files.append(file_part) css_files.append(file_part)
else: else:
css_files.extend(file_part) css_files.extend(file_part)
@@ -564,13 +567,13 @@ class RequestHandler(object):
js = ''.join('<script src="' + escape.xhtml_escape(p) + js = ''.join('<script src="' + escape.xhtml_escape(p) +
'" type="text/javascript"></script>' '" type="text/javascript"></script>'
for p in paths) for p in paths)
sloc = html.rindex(b('</body>')) sloc = html.rindex(b'</body>')
html = html[:sloc] + utf8(js) + b('\n') + html[sloc:] html = html[:sloc] + utf8(js) + b'\n' + html[sloc:]
if js_embed: if js_embed:
js = b('<script type="text/javascript">\n//<![CDATA[\n') + \ js = b'<script type="text/javascript">\n//<![CDATA[\n' + \
b('\n').join(js_embed) + b('\n//]]>\n</script>') b'\n'.join(js_embed) + b'\n//]]>\n</script>'
sloc = html.rindex(b('</body>')) sloc = html.rindex(b'</body>')
html = html[:sloc] + js + b('\n') + html[sloc:] html = html[:sloc] + js + b'\n' + html[sloc:]
if css_files: if css_files:
paths = [] paths = []
unique_paths = set() unique_paths = set()
@@ -583,19 +586,19 @@ class RequestHandler(object):
css = ''.join('<link href="' + escape.xhtml_escape(p) + '" ' css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
'type="text/css" rel="stylesheet"/>' 'type="text/css" rel="stylesheet"/>'
for p in paths) for p in paths)
hloc = html.index(b('</head>')) hloc = html.index(b'</head>')
html = html[:hloc] + utf8(css) + b('\n') + html[hloc:] html = html[:hloc] + utf8(css) + b'\n' + html[hloc:]
if css_embed: if css_embed:
css = b('<style type="text/css">\n') + b('\n').join(css_embed) + \ css = b'<style type="text/css">\n' + b'\n'.join(css_embed) + \
b('\n</style>') b'\n</style>'
hloc = html.index(b('</head>')) hloc = html.index(b'</head>')
html = html[:hloc] + css + b('\n') + html[hloc:] html = html[:hloc] + css + b'\n' + html[hloc:]
if html_heads: if html_heads:
hloc = html.index(b('</head>')) hloc = html.index(b'</head>')
html = html[:hloc] + b('').join(html_heads) + b('\n') + html[hloc:] html = html[:hloc] + b''.join(html_heads) + b'\n' + html[hloc:]
if html_bodies: if html_bodies:
hloc = html.index(b('</body>')) hloc = html.index(b'</body>')
html = html[:hloc] + b('').join(html_bodies) + b('\n') + html[hloc:] html = html[:hloc] + b''.join(html_bodies) + b'\n' + html[hloc:]
self.finish(html) self.finish(html)
def render_string(self, template_name, **kwargs): def render_string(self, template_name, **kwargs):
@@ -675,19 +678,19 @@ class RequestHandler(object):
if self.application._wsgi: if self.application._wsgi:
raise Exception("WSGI applications do not support flush()") raise Exception("WSGI applications do not support flush()")
chunk = b("").join(self._write_buffer) chunk = b"".join(self._write_buffer)
self._write_buffer = [] self._write_buffer = []
if not self._headers_written: if not self._headers_written:
self._headers_written = True self._headers_written = True
for transform in self._transforms: for transform in self._transforms:
self._status_code, self._headers, chunk = \ self._status_code, self._headers, chunk = \
transform.transform_first_chunk( transform.transform_first_chunk(
self._status_code, self._headers, chunk, include_footers) self._status_code, self._headers, chunk, include_footers)
headers = self._generate_headers() headers = self._generate_headers()
else: else:
for transform in self._transforms: for transform in self._transforms:
chunk = transform.transform_chunk(chunk, include_footers) chunk = transform.transform_chunk(chunk, include_footers)
headers = b("") headers = b""
# Ignore the chunk and only write the headers for HEAD requests # Ignore the chunk and only write the headers for HEAD requests
if self.request.method == "HEAD": if self.request.method == "HEAD":
@@ -712,7 +715,7 @@ class RequestHandler(object):
if not self._headers_written: if not self._headers_written:
if (self._status_code == 200 and if (self._status_code == 200 and
self.request.method in ("GET", "HEAD") and self.request.method in ("GET", "HEAD") and
"Etag" not in self._headers): "Etag" not in self._headers):
etag = self.compute_etag() etag = self.compute_etag()
if etag is not None: if etag is not None:
self.set_header("Etag", etag) self.set_header("Etag", etag)
@@ -812,9 +815,9 @@ class RequestHandler(object):
else: else:
self.finish("<html><title>%(code)d: %(message)s</title>" self.finish("<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>" % { "<body>%(code)d: %(message)s</body></html>" % {
"code": status_code, "code": status_code,
"message": self._reason, "message": self._reason,
}) })
@property @property
def locale(self): def locale(self):
@@ -861,7 +864,7 @@ class RequestHandler(object):
score = 1.0 score = 1.0
locales.append((parts[0], score)) locales.append((parts[0], score))
if locales: if locales:
locales.sort(key=lambda (l, s): s, reverse=True) locales.sort(key=lambda pair: pair[1], reverse=True)
codes = [l[0] for l in locales] codes = [l[0] for l in locales]
return locale.get(*codes) return locale.get(*codes)
return locale.get(default) return locale.get(default)
@@ -1007,7 +1010,7 @@ class RequestHandler(object):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
return callback(*args, **kwargs) return callback(*args, **kwargs)
except Exception, e: except Exception as e:
if self._headers_written: if self._headers_written:
app_log.error("Exception after headers written", app_log.error("Exception after headers written",
exc_info=True) exc_info=True)
@@ -1055,11 +1058,11 @@ class RequestHandler(object):
raise HTTPError(405) raise HTTPError(405)
self.path_args = [self.decode_argument(arg) for arg in args] self.path_args = [self.decode_argument(arg) for arg in args]
self.path_kwargs = dict((k, self.decode_argument(v, name=k)) self.path_kwargs = dict((k, self.decode_argument(v, name=k))
for (k, v) in kwargs.iteritems()) for (k, v) in kwargs.items())
# If XSRF cookies are turned on, reject form submissions without # If XSRF cookies are turned on, reject form submissions without
# the proper cookie # the proper cookie
if self.request.method not in ("GET", "HEAD", "OPTIONS") and \ if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
self.application.settings.get("xsrf_cookies"): self.application.settings.get("xsrf_cookies"):
self.check_xsrf_cookie() self.check_xsrf_cookie()
self.prepare() self.prepare()
if not self._finished: if not self._finished:
@@ -1067,7 +1070,7 @@ class RequestHandler(object):
*self.path_args, **self.path_kwargs) *self.path_args, **self.path_kwargs)
if self._auto_finish and not self._finished: if self._auto_finish and not self._finished:
self.finish() self.finish()
except Exception, e: except Exception as e:
self._handle_request_exception(e) self._handle_request_exception(e)
def _generate_headers(self): def _generate_headers(self):
@@ -1075,12 +1078,12 @@ class RequestHandler(object):
lines = [utf8(self.request.version + " " + lines = [utf8(self.request.version + " " +
str(self._status_code) + str(self._status_code) +
" " + reason)] " " + reason)]
lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in lines.extend([utf8(n) + b": " + utf8(v) for n, v in self._headers.get_all()])
itertools.chain(self._headers.iteritems(), self._list_headers)])
if hasattr(self, "_new_cookie"): if hasattr(self, "_new_cookie"):
for cookie in self._new_cookie.values(): for cookie in self._new_cookie.values():
lines.append(utf8("Set-Cookie: " + cookie.OutputString(None))) lines.append(utf8("Set-Cookie: " + cookie.OutputString(None)))
return b("\r\n").join(lines) + b("\r\n\r\n") return b"\r\n".join(lines) + b"\r\n\r\n"
def _log(self): def _log(self):
"""Logs the current request. """Logs the current request.
@@ -1101,7 +1104,7 @@ class RequestHandler(object):
format = "%d %s: " + e.log_message format = "%d %s: " + e.log_message
args = [e.status_code, self._request_summary()] + list(e.args) args = [e.status_code, self._request_summary()] + list(e.args)
gen_log.warning(format, *args) gen_log.warning(format, *args)
if e.status_code not in httplib.responses and not e.reason: if e.status_code not in httputil.responses and not e.reason:
gen_log.error("Bad HTTP status code: %d", e.status_code) gen_log.error("Bad HTTP status code: %d", e.status_code)
self.send_error(500, exc_info=sys.exc_info()) self.send_error(500, exc_info=sys.exc_info())
else: else:
@@ -1161,7 +1164,7 @@ def asynchronous(method):
raise Exception("@asynchronous is not supported for WSGI apps") raise Exception("@asynchronous is not supported for WSGI apps")
self._auto_finish = False self._auto_finish = False
with stack_context.ExceptionStackContext( with stack_context.ExceptionStackContext(
self._stack_context_handle_exception): self._stack_context_handle_exception):
return method(self, *args, **kwargs) return method(self, *args, **kwargs)
return wrapper return wrapper
@@ -1334,7 +1337,7 @@ class Application(object):
self.handlers.append((re.compile(host_pattern), handlers)) self.handlers.append((re.compile(host_pattern), handlers))
for spec in host_handlers: for spec in host_handlers:
if type(spec) is type(()): if isinstance(spec, type(())):
assert len(spec) in (2, 3) assert len(spec) in (2, 3)
pattern = spec[0] pattern = spec[0]
handler = spec[1] handler = spec[1]
@@ -1375,20 +1378,20 @@ class Application(object):
return matches or None return matches or None
def _load_ui_methods(self, methods): def _load_ui_methods(self, methods):
if type(methods) is types.ModuleType: if isinstance(methods, types.ModuleType):
self._load_ui_methods(dict((n, getattr(methods, n)) self._load_ui_methods(dict((n, getattr(methods, n))
for n in dir(methods))) for n in dir(methods)))
elif isinstance(methods, list): elif isinstance(methods, list):
for m in methods: for m in methods:
self._load_ui_methods(m) self._load_ui_methods(m)
else: else:
for name, fn in methods.iteritems(): for name, fn in methods.items():
if not name.startswith("_") and hasattr(fn, "__call__") \ if not name.startswith("_") and hasattr(fn, "__call__") \
and name[0].lower() == name[0]: and name[0].lower() == name[0]:
self.ui_methods[name] = fn self.ui_methods[name] = fn
def _load_ui_modules(self, modules): def _load_ui_modules(self, modules):
if type(modules) is types.ModuleType: if isinstance(modules, types.ModuleType):
self._load_ui_modules(dict((n, getattr(modules, n)) self._load_ui_modules(dict((n, getattr(modules, n))
for n in dir(modules))) for n in dir(modules)))
elif isinstance(modules, list): elif isinstance(modules, list):
@@ -1396,7 +1399,7 @@ class Application(object):
self._load_ui_modules(m) self._load_ui_modules(m)
else: else:
assert isinstance(modules, dict) assert isinstance(modules, dict)
for name, cls in modules.iteritems(): for name, cls in modules.items():
try: try:
if issubclass(cls, UIModule): if issubclass(cls, UIModule):
self.ui_modules[name] = cls self.ui_modules[name] = cls
@@ -1434,7 +1437,7 @@ class Application(object):
if spec.regex.groupindex: if spec.regex.groupindex:
kwargs = dict( kwargs = dict(
(str(k), unquote(v)) (str(k), unquote(v))
for (k, v) in match.groupdict().iteritems()) for (k, v) in match.groupdict().items())
else: else:
args = [unquote(s) for s in match.groups()] args = [unquote(s) for s in match.groups()]
break break
@@ -1510,7 +1513,7 @@ class HTTPError(Exception):
def __str__(self): def __str__(self):
message = "HTTP %d: %s" % ( message = "HTTP %d: %s" % (
self.status_code, self.status_code,
self.reason or httplib.responses.get(self.status_code, 'Unknown')) self.reason or httputil.responses.get(self.status_code, 'Unknown'))
if self.log_message: if self.log_message:
return message + " (" + (self.log_message % self.args) + ")" return message + " (" + (self.log_message % self.args) + ")"
else: else:
@@ -1618,7 +1621,7 @@ class StaticFileHandler(RequestHandler):
if cache_time > 0: if cache_time > 0:
self.set_header("Expires", datetime.datetime.utcnow() + self.set_header("Expires", datetime.datetime.utcnow() +
datetime.timedelta(seconds=cache_time)) datetime.timedelta(seconds=cache_time))
self.set_header("Cache-Control", "max-age=" + str(cache_time)) self.set_header("Cache-Control", "max-age=" + str(cache_time))
self.set_extra_headers(path) self.set_extra_headers(path)
@@ -1771,9 +1774,9 @@ class GZipContentEncoding(OutputTransform):
def transform_first_chunk(self, status_code, headers, chunk, finishing): def transform_first_chunk(self, status_code, headers, chunk, finishing):
if 'Vary' in headers: if 'Vary' in headers:
headers['Vary'] += b(', Accept-Encoding') headers['Vary'] += b', Accept-Encoding'
else: else:
headers['Vary'] = b('Accept-Encoding') headers['Vary'] = b'Accept-Encoding'
if self._gzipping: if self._gzipping:
ctype = _unicode(headers.get("Content-Type", "")).split(";")[0] ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
self._gzipping = (ctype in self.CONTENT_TYPES) and \ self._gzipping = (ctype in self.CONTENT_TYPES) and \
@@ -1827,9 +1830,9 @@ class ChunkedTransferEncoding(OutputTransform):
# Don't write out empty chunks because that means END-OF-STREAM # Don't write out empty chunks because that means END-OF-STREAM
# with chunked encoding # with chunked encoding
if block: if block:
block = utf8("%x" % len(block)) + b("\r\n") + block + b("\r\n") block = utf8("%x" % len(block)) + b"\r\n" + block + b"\r\n"
if finishing: if finishing:
block += b("0\r\n\r\n") block += b"0\r\n\r\n"
return block return block
@@ -1846,7 +1849,7 @@ def authenticated(method):
next_url = self.request.full_url() next_url = self.request.full_url()
else: else:
next_url = self.request.uri next_url = self.request.uri
url += "?" + urllib.urlencode(dict(next=next_url)) url += "?" + urlencode(dict(next=next_url))
self.redirect(url) self.redirect(url)
return return
raise HTTPError(403) raise HTTPError(403)
@@ -1954,7 +1957,7 @@ class TemplateModule(UIModule):
def javascript_files(self): def javascript_files(self):
result = [] result = []
for f in self._get_resources("javascript_files"): for f in self._get_resources("javascript_files"):
if isinstance(f, (unicode, bytes_type)): if isinstance(f, (unicode_type, bytes_type)):
result.append(f) result.append(f)
else: else:
result.extend(f) result.extend(f)
@@ -1966,7 +1969,7 @@ class TemplateModule(UIModule):
def css_files(self): def css_files(self):
result = [] result = []
for f in self._get_resources("css_files"): for f in self._get_resources("css_files"):
if isinstance(f, (unicode, bytes_type)): if isinstance(f, (unicode_type, bytes_type)):
result.append(f) result.append(f)
else: else:
result.extend(f) result.extend(f)
@@ -2011,8 +2014,8 @@ class URLSpec(object):
def __repr__(self): def __repr__(self):
return '%s(%r, %s, kwargs=%r, name=%r)' % \ return '%s(%r, %s, kwargs=%r, name=%r)' % \
(self.__class__.__name__, self.regex.pattern, (self.__class__.__name__, self.regex.pattern,
self.handler_class, self.kwargs, self.name) self.handler_class, self.kwargs, self.name)
def _find_groups(self): def _find_groups(self):
"""Returns a tuple (reverse string, group count) for a url. """Returns a tuple (reverse string, group count) for a url.
@@ -2051,7 +2054,7 @@ class URLSpec(object):
return self._path return self._path
converted_args = [] converted_args = []
for a in args: for a in args:
if not isinstance(a, (unicode, bytes_type)): if not isinstance(a, (unicode_type, bytes_type)):
a = str(a) a = str(a)
converted_args.append(escape.url_escape(utf8(a))) converted_args.append(escape.url_escape(utf8(a)))
return self._path % tuple(converted_args) return self._path % tuple(converted_args)
@@ -2066,7 +2069,7 @@ else:
if len(a) != len(b): if len(a) != len(b):
return False return False
result = 0 result = 0
if type(a[0]) is int: # python3 byte strings if isinstance(a[0], int): # python3 byte strings
for x, y in zip(a, b): for x, y in zip(a, b):
result |= x ^ y result |= x ^ y
else: # python2 else: # python2
@@ -2079,14 +2082,14 @@ def create_signed_value(secret, name, value):
timestamp = utf8(str(int(time.time()))) timestamp = utf8(str(int(time.time())))
value = base64.b64encode(utf8(value)) value = base64.b64encode(utf8(value))
signature = _create_signature(secret, name, value, timestamp) signature = _create_signature(secret, name, value, timestamp)
value = b("|").join([value, timestamp, signature]) value = b"|".join([value, timestamp, signature])
return value return value
def decode_signed_value(secret, name, value, max_age_days=31): def decode_signed_value(secret, name, value, max_age_days=31):
if not value: if not value:
return None return None
parts = utf8(value).split(b("|")) parts = utf8(value).split(b"|")
if len(parts) != 3: if len(parts) != 3:
return None return None
signature = _create_signature(secret, name, parts[0], parts[1]) signature = _create_signature(secret, name, parts[0], parts[1])
@@ -2105,7 +2108,7 @@ def decode_signed_value(secret, name, value, max_age_days=31):
# here instead of modifying _cookie_signature. # here instead of modifying _cookie_signature.
gen_log.warning("Cookie timestamp in future; possible tampering %r", value) gen_log.warning("Cookie timestamp in future; possible tampering %r", value)
return None return None
if parts[1].startswith(b("0")): if parts[1].startswith(b"0"):
gen_log.warning("Tampered cookie %r", value) gen_log.warning("Tampered cookie %r", value)
return None return None
try: try:
+174 -40
View File
@@ -17,21 +17,42 @@ communication between the browser and server.
documentation for caveats). documentation for caveats).
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
# Author: Jacob Kristhammar, 2010 # Author: Jacob Kristhammar, 2010
import array import array
import base64
import collections
import functools import functools
import hashlib import hashlib
import logging
import os
import re
import socket
import struct import struct
import time import time
import base64
import tornado.escape import tornado.escape
import tornado.web import tornado.web
from tornado.concurrent import Future, return_future
from tornado.escape import utf8, to_unicode, native_str
from tornado.httputil import HTTPHeaders
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream
from tornado.log import gen_log, app_log from tornado.log import gen_log, app_log
from tornado.util import bytes_type, b from tornado.netutil import Resolver
from tornado import simple_httpclient
from tornado.util import bytes_type
try:
xrange # py2
except NameError:
xrange = range # py3
try:
import urlparse # py2
except ImportError:
import urllib.parse as urlparse # py3
class WebSocketHandler(tornado.web.RequestHandler): class WebSocketHandler(tornado.web.RequestHandler):
"""Subclass this class to create a basic WebSocket handler. """Subclass this class to create a basic WebSocket handler.
@@ -325,12 +346,12 @@ class WebSocketProtocol76(WebSocketProtocol):
"Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n" "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
"%(subprotocol)s" "%(subprotocol)s"
"\r\n" % (dict( "\r\n" % (dict(
version=tornado.version, version=tornado.version,
origin=self.request.headers["Origin"], origin=self.request.headers["Origin"],
scheme=scheme, scheme=scheme,
host=self.request.host, host=self.request.host,
uri=self.request.uri, uri=self.request.uri,
subprotocol=subprotocol_header)))) subprotocol=subprotocol_header))))
self.stream.read_bytes(8, self._handle_challenge) self.stream.read_bytes(8, self._handle_challenge)
def challenge_response(self, challenge): def challenge_response(self, challenge):
@@ -398,7 +419,7 @@ class WebSocketProtocol76(WebSocketProtocol):
def _on_frame_type(self, byte): def _on_frame_type(self, byte):
frame_type = ord(byte) frame_type = ord(byte)
if frame_type == 0x00: if frame_type == 0x00:
self.stream.read_until(b("\xff"), self._on_end_delimiter) self.stream.read_until(b"\xff", self._on_end_delimiter)
elif frame_type == 0xff: elif frame_type == 0xff:
self.stream.read_bytes(1, self._on_length_indicator) self.stream.read_bytes(1, self._on_length_indicator)
else: else:
@@ -407,7 +428,7 @@ class WebSocketProtocol76(WebSocketProtocol):
def _on_end_delimiter(self, frame): def _on_end_delimiter(self, frame):
if not self.client_terminated: if not self.client_terminated:
self.async_callback(self.handler.on_message)( self.async_callback(self.handler.on_message)(
frame[:-1].decode("utf-8", "replace")) frame[:-1].decode("utf-8", "replace"))
if not self.client_terminated: if not self.client_terminated:
self._receive_message() self._receive_message()
@@ -426,7 +447,7 @@ class WebSocketProtocol76(WebSocketProtocol):
if isinstance(message, unicode): if isinstance(message, unicode):
message = message.encode("utf-8") message = message.encode("utf-8")
assert isinstance(message, bytes_type) assert isinstance(message, bytes_type)
self.stream.write(b("\x00") + message + b("\xff")) self.stream.write(b"\x00" + message + b"\xff")
def write_ping(self, data): def write_ping(self, data):
"""Send ping frame.""" """Send ping frame."""
@@ -454,10 +475,12 @@ class WebSocketProtocol13(WebSocketProtocol):
This class supports versions 7 and 8 of the protocol in addition to the This class supports versions 7 and 8 of the protocol in addition to the
final version 13. final version 13.
""" """
def __init__(self, handler): def __init__(self, handler, mask_outgoing=False):
WebSocketProtocol.__init__(self, handler) WebSocketProtocol.__init__(self, handler)
self.mask_outgoing = mask_outgoing
self._final_frame = False self._final_frame = False
self._frame_opcode = None self._frame_opcode = None
self._masked_frame = None
self._frame_mask = None self._frame_mask = None
self._frame_length = None self._frame_length = None
self._fragmented_message_buffer = None self._fragmented_message_buffer = None
@@ -469,7 +492,7 @@ class WebSocketProtocol13(WebSocketProtocol):
self._handle_websocket_headers() self._handle_websocket_headers()
self._accept_connection() self._accept_connection()
except ValueError: except ValueError:
gen_log.debug("Malformed WebSocket request received") gen_log.debug("Malformed WebSocket request received", exc_info=True)
self._abort() self._abort()
return return
@@ -483,12 +506,20 @@ class WebSocketProtocol13(WebSocketProtocol):
if not all(map(lambda f: self.request.headers.get(f), fields)): if not all(map(lambda f: self.request.headers.get(f), fields)):
raise ValueError("Missing/Invalid WebSocket headers") raise ValueError("Missing/Invalid WebSocket headers")
def _challenge_response(self): @staticmethod
def compute_accept_value(key):
"""Computes the value for the Sec-WebSocket-Accept header,
given the value for Sec-WebSocket-Key.
"""
sha1 = hashlib.sha1() sha1 = hashlib.sha1()
sha1.update(tornado.escape.utf8( sha1.update(utf8(key))
self.request.headers.get("Sec-Websocket-Key"))) sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11") # Magic value
sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value return native_str(base64.b64encode(sha1.digest()))
return tornado.escape.native_str(base64.b64encode(sha1.digest()))
def _challenge_response(self):
return WebSocketProtocol13.compute_accept_value(
self.request.headers.get("Sec-Websocket-Key"))
def _accept_connection(self): def _accept_connection(self):
subprotocol_header = '' subprotocol_header = ''
@@ -518,12 +549,19 @@ class WebSocketProtocol13(WebSocketProtocol):
finbit = 0 finbit = 0
frame = struct.pack("B", finbit | opcode) frame = struct.pack("B", finbit | opcode)
l = len(data) l = len(data)
if l < 126: if self.mask_outgoing:
frame += struct.pack("B", l) mask_bit = 0x80
elif l <= 0xFFFF:
frame += struct.pack("!BH", 126, l)
else: else:
frame += struct.pack("!BQ", 127, l) mask_bit = 0
if l < 126:
frame += struct.pack("B", l | mask_bit)
elif l <= 0xFFFF:
frame += struct.pack("!BH", 126 | mask_bit, l)
else:
frame += struct.pack("!BQ", 127 | mask_bit, l)
if self.mask_outgoing:
mask = os.urandom(4)
data = mask + self._apply_mask(mask, data)
frame += data frame += data
self.stream.write(frame) self.stream.write(frame)
@@ -555,10 +593,7 @@ class WebSocketProtocol13(WebSocketProtocol):
# client is using as-yet-undefined extensions; abort # client is using as-yet-undefined extensions; abort
self._abort() self._abort()
return return
if not (payloadlen & 0x80): self._masked_frame = bool(payloadlen & 0x80)
# Unmasked frame -> abort connection
self._abort()
return
payloadlen = payloadlen & 0x7f payloadlen = payloadlen & 0x7f
if self._frame_opcode_is_control and payloadlen >= 126: if self._frame_opcode_is_control and payloadlen >= 126:
# control frames must have payload < 126 # control frames must have payload < 126
@@ -566,7 +601,10 @@ class WebSocketProtocol13(WebSocketProtocol):
return return
if payloadlen < 126: if payloadlen < 126:
self._frame_length = payloadlen self._frame_length = payloadlen
self.stream.read_bytes(4, self._on_masking_key) if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key)
else:
self.stream.read_bytes(self._frame_length, self._on_frame_data)
elif payloadlen == 126: elif payloadlen == 126:
self.stream.read_bytes(2, self._on_frame_length_16) self.stream.read_bytes(2, self._on_frame_length_16)
elif payloadlen == 127: elif payloadlen == 127:
@@ -574,21 +612,39 @@ class WebSocketProtocol13(WebSocketProtocol):
def _on_frame_length_16(self, data): def _on_frame_length_16(self, data):
self._frame_length = struct.unpack("!H", data)[0] self._frame_length = struct.unpack("!H", data)[0]
self.stream.read_bytes(4, self._on_masking_key) if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key)
else:
self.stream.read_bytes(self._frame_length, self._on_frame_data)
def _on_frame_length_64(self, data): def _on_frame_length_64(self, data):
self._frame_length = struct.unpack("!Q", data)[0] self._frame_length = struct.unpack("!Q", data)[0]
self.stream.read_bytes(4, self._on_masking_key) if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key)
else:
self.stream.read_bytes(self._frame_length, self._on_frame_data)
def _on_masking_key(self, data): def _on_masking_key(self, data):
self._frame_mask = array.array("B", data) self._frame_mask = data
self.stream.read_bytes(self._frame_length, self._on_frame_data) self.stream.read_bytes(self._frame_length, self._on_masked_frame_data)
def _on_frame_data(self, data): def _apply_mask(self, mask, data):
mask = array.array("B", mask)
unmasked = array.array("B", data) unmasked = array.array("B", data)
for i in xrange(len(data)): for i in xrange(len(data)):
unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4] unmasked[i] = unmasked[i] ^ mask[i % 4]
if hasattr(unmasked, 'tobytes'):
# tostring was deprecated in py32. It hasn't been removed,
# but since we turn on deprecation warnings in our tests
# we need to use the right one.
return unmasked.tobytes()
else:
return unmasked.tostring()
def _on_masked_frame_data(self, data):
self._on_frame_data(self._apply_mask(self._frame_mask, data))
def _on_frame_data(self, data):
if self._frame_opcode_is_control: if self._frame_opcode_is_control:
# control frames may be interleaved with a series of fragmented # control frames may be interleaved with a series of fragmented
# data frames, so control frames must not interact with # data frames, so control frames must not interact with
@@ -603,10 +659,10 @@ class WebSocketProtocol13(WebSocketProtocol):
# nothing to continue # nothing to continue
self._abort() self._abort()
return return
self._fragmented_message_buffer += unmasked self._fragmented_message_buffer += data
if self._final_frame: if self._final_frame:
opcode = self._fragmented_message_opcode opcode = self._fragmented_message_opcode
unmasked = self._fragmented_message_buffer data = self._fragmented_message_buffer
self._fragmented_message_buffer = None self._fragmented_message_buffer = None
else: # start of new data message else: # start of new data message
if self._fragmented_message_buffer is not None: if self._fragmented_message_buffer is not None:
@@ -617,10 +673,10 @@ class WebSocketProtocol13(WebSocketProtocol):
opcode = self._frame_opcode opcode = self._frame_opcode
else: else:
self._fragmented_message_opcode = self._frame_opcode self._fragmented_message_opcode = self._frame_opcode
self._fragmented_message_buffer = unmasked self._fragmented_message_buffer = data
if self._final_frame: if self._final_frame:
self._handle_message(opcode, unmasked.tostring()) self._handle_message(opcode, data)
if not self.client_terminated: if not self.client_terminated:
self._receive_frame() self._receive_frame()
@@ -657,7 +713,7 @@ class WebSocketProtocol13(WebSocketProtocol):
"""Closes the WebSocket connection.""" """Closes the WebSocket connection."""
if not self.server_terminated: if not self.server_terminated:
if not self.stream.closed(): if not self.stream.closed():
self._write_frame(True, 0x8, b("")) self._write_frame(True, 0x8, b"")
self.server_terminated = True self.server_terminated = True
if self.client_terminated: if self.client_terminated:
if self._waiting is not None: if self._waiting is not None:
@@ -669,3 +725,81 @@ class WebSocketProtocol13(WebSocketProtocol):
# otherwise just close the connection. # otherwise just close the connection.
self._waiting = self.stream.io_loop.add_timeout( self._waiting = self.stream.io_loop.add_timeout(
self.stream.io_loop.time() + 5, self._abort) self.stream.io_loop.time() + 5, self._abort)
class _WebSocketClientConnection(simple_httpclient._HTTPConnection):
def __init__(self, io_loop, request):
self.connect_future = Future()
self.read_future = None
self.read_queue = collections.deque()
self.key = base64.b64encode(os.urandom(16))
scheme, sep, rest = request.url.partition(':')
scheme = {'ws': 'http', 'wss': 'https'}[scheme]
request.url = scheme + sep + rest
request.headers.update({
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Key': self.key,
'Sec-WebSocket-Version': '13',
})
super(_WebSocketClientConnection, self).__init__(
io_loop, None, request, lambda: None, lambda response: None,
104857600, Resolver(io_loop=io_loop))
def _on_close(self):
self.on_message(None)
def _handle_1xx(self, code):
assert code == 101
assert self.headers['Upgrade'].lower() == 'websocket'
assert self.headers['Connection'].lower() == 'upgrade'
accept = WebSocketProtocol13.compute_accept_value(self.key)
assert self.headers['Sec-Websocket-Accept'] == accept
self.protocol = WebSocketProtocol13(self, mask_outgoing=True)
self.protocol._receive_frame()
if self._timeout is not None:
self.io_loop.remove_timeout(self._timeout)
self._timeout = None
self.connect_future.set_result(self)
def write_message(self, message, binary=False):
self.protocol.write_message(message, binary)
def read_message(self, callback=None):
assert self.read_future is None
future = Future()
if self.read_queue:
future.set_result(self.read_queue.popleft())
else:
self.read_future = future
if callback is not None:
self.io_loop.add_future(future, callback)
return future
def on_message(self, message):
if self.read_future is not None:
self.read_future.set_result(message)
self.read_future = None
else:
self.read_queue.append(message)
def on_pong(self, data):
pass
def WebSocketConnect(url, io_loop=None, callback=None):
if io_loop is None:
io_loop = IOLoop.instance()
request = simple_httpclient.HTTPRequest(url)
request = simple_httpclient._RequestProxy(
request, simple_httpclient.HTTPRequest._DEFAULTS)
conn = _WebSocketClientConnection(io_loop, request)
if callback is not None:
io_loop.add_future(conn.connect_future, callback)
return conn.connect_future
+21 -15
View File
@@ -29,33 +29,39 @@ provides WSGI support in two ways:
and Tornado handlers in a single server. and Tornado handlers in a single server.
""" """
from __future__ import absolute_import, division, with_statement from __future__ import absolute_import, division, print_function, with_statement
import Cookie
import httplib
import sys import sys
import time import time
import tornado import tornado
import urllib
from tornado import escape from tornado import escape
from tornado import httputil from tornado import httputil
from tornado.log import access_log from tornado.log import access_log
from tornado import web from tornado import web
from tornado.escape import native_str, utf8, parse_qs_bytes from tornado.escape import native_str, parse_qs_bytes
from tornado.util import b, bytes_type from tornado.util import bytes_type, unicode_type
try: try:
from io import BytesIO # python 3 from io import BytesIO # python 3
except ImportError: except ImportError:
from cStringIO import StringIO as BytesIO # python 2 from cStringIO import StringIO as BytesIO # python 2
try:
import Cookie # py2
except ImportError:
import http.cookies as Cookie # py3
try:
import urllib.parse as urllib_parse # py3
except ImportError:
import urllib as urllib_parse
# PEP 3333 specifies that WSGI on python 3 generally deals with byte strings # PEP 3333 specifies that WSGI on python 3 generally deals with byte strings
# that are smuggled inside objects of type unicode (via the latin1 encoding). # that are smuggled inside objects of type unicode (via the latin1 encoding).
# These functions are like those in the tornado.escape module, but defined # These functions are like those in the tornado.escape module, but defined
# here to minimize the temptation to use them in non-wsgi contexts. # here to minimize the temptation to use them in non-wsgi contexts.
if str is unicode: if str is unicode_type:
def to_wsgi_str(s): def to_wsgi_str(s):
assert isinstance(s, bytes_type) assert isinstance(s, bytes_type)
return s.decode('latin1') return s.decode('latin1')
@@ -116,7 +122,7 @@ class WSGIApplication(web.Application):
assert handler._finished assert handler._finished
reason = handler._reason reason = handler._reason
status = str(handler._status_code) + " " + reason status = str(handler._status_code) + " " + reason
headers = handler._headers.items() + handler._list_headers headers = list(handler._headers.get_all())
if hasattr(handler, "_new_cookie"): if hasattr(handler, "_new_cookie"):
for cookie in handler._new_cookie.values(): for cookie in handler._new_cookie.values():
headers.append(("Set-Cookie", cookie.OutputString(None))) headers.append(("Set-Cookie", cookie.OutputString(None)))
@@ -130,8 +136,8 @@ class HTTPRequest(object):
def __init__(self, environ): def __init__(self, environ):
"""Parses the given WSGI environ to construct the request.""" """Parses the given WSGI environ to construct the request."""
self.method = environ["REQUEST_METHOD"] self.method = environ["REQUEST_METHOD"]
self.path = urllib.quote(from_wsgi_str(environ.get("SCRIPT_NAME", ""))) self.path = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", "")))
self.path += urllib.quote(from_wsgi_str(environ.get("PATH_INFO", ""))) self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", "")))
self.uri = self.path self.uri = self.path
self.arguments = {} self.arguments = {}
self.query = environ.get("QUERY_STRING", "") self.query = environ.get("QUERY_STRING", "")
@@ -235,7 +241,7 @@ class WSGIContainer(object):
app_response = self.wsgi_application( app_response = self.wsgi_application(
WSGIContainer.environ(request), start_response) WSGIContainer.environ(request), start_response)
response.extend(app_response) response.extend(app_response)
body = b("").join(response) body = b"".join(response)
if hasattr(app_response, "close"): if hasattr(app_response, "close"):
app_response.close() app_response.close()
if not data: if not data:
@@ -255,10 +261,10 @@ class WSGIContainer(object):
parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")] parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
for key, value in headers: for key, value in headers:
parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n")) parts.append(escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n")
parts.append(b("\r\n")) parts.append(b"\r\n")
parts.append(body) parts.append(body)
request.write(b("").join(parts)) request.write(b"".join(parts))
request.finish() request.finish()
self._log(status_code, request) self._log(status_code, request)
@@ -294,7 +300,7 @@ class WSGIContainer(object):
environ["CONTENT_TYPE"] = request.headers.pop("Content-Type") environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
if "Content-Length" in request.headers: if "Content-Length" in request.headers:
environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length") environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
for key, value in request.headers.iteritems(): for key, value in request.headers.items():
environ["HTTP_" + key.replace("-", "_").upper()] = value environ["HTTP_" + key.replace("-", "_").upper()] = value
return environ return environ