Update Tornado lib

This commit is contained in:
Ruud
2012-12-03 13:31:34 +01:00
parent f0a3358561
commit 802338a934
33 changed files with 2510 additions and 1336 deletions
+129 -67
View File
@@ -63,7 +63,6 @@ import hashlib
import hmac
import httplib
import itertools
import logging
import mimetypes
import os.path
import re
@@ -80,6 +79,7 @@ import uuid
from tornado import escape
from tornado import locale
from tornado.log import access_log, app_log, gen_log
from tornado import stack_context
from tornado import template
from tornado.escape import utf8, _unicode
@@ -105,12 +105,16 @@ class RequestHandler(object):
_template_loader_lock = threading.Lock()
def __init__(self, application, request, **kwargs):
super(RequestHandler, self).__init__()
self.application = application
self.request = request
self._headers_written = False
self._finished = False
self._auto_finish = True
self._transforms = None # will be set in _execute
self.path_args = None
self.path_kwargs = None
self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
application.ui_methods.iteritems())
# UIModules are available as both `modules` and `_modules` in the
@@ -219,6 +223,8 @@ class RequestHandler(object):
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()
@@ -227,6 +233,7 @@ class RequestHandler(object):
self.set_header("Connection", "Keep-Alive")
self._write_buffer = []
self._status_code = 200
self._reason = httplib.responses[200]
def set_default_headers(self):
"""Override this to set HTTP headers at the beginning of the request.
@@ -238,10 +245,22 @@ class RequestHandler(object):
"""
pass
def set_status(self, status_code):
"""Sets the status code for our response."""
assert status_code in httplib.responses
def set_status(self, status_code, reason=None):
"""Sets the status code for our response.
:arg int status_code: Response status code. If `reason` is ``None``,
it must be present in `httplib.responses`.
:arg string reason: Human-readable reason phrase describing the status
code. If ``None``, it will be filled in from `httplib.responses`.
"""
self._status_code = status_code
if reason is not None:
self._reason = escape.native_str(reason)
else:
try:
self._reason = httplib.responses[status_code]
except KeyError:
raise ValueError("unknown status code %d", status_code)
def get_status(self):
"""Returns the status code for our response."""
@@ -600,7 +619,20 @@ class RequestHandler(object):
else:
loader = RequestHandler._template_loaders[template_path]
t = loader.load(template_name)
args = dict(
namespace = self.get_template_namespace()
namespace.update(kwargs)
return t.generate(**namespace)
def get_template_namespace(self):
"""Returns a dictionary to be used as the default template namespace.
May be overridden by subclasses to add or modify values.
The results of this method will be combined with additional
defaults in the `tornado.template` module and keyword arguments
to `render` or `render_string`.
"""
namespace = dict(
handler=self,
request=self.request,
current_user=self.current_user,
@@ -610,11 +642,17 @@ class RequestHandler(object):
xsrf_form_html=self.xsrf_form_html,
reverse_url=self.reverse_url
)
args.update(self.ui)
args.update(kwargs)
return t.generate(**args)
namespace.update(self.ui)
return namespace
def create_template_loader(self, template_path):
"""Returns a new template loader for the given path.
May be overridden by subclasses. By default returns a
directory-based loader on the given path, using the
``autoescape`` application setting. If a ``template_loader``
application setting is supplied, uses that instead.
"""
settings = self.application.settings
if "template_loader" in settings:
return settings["template_loader"]
@@ -715,16 +753,22 @@ class RequestHandler(object):
Additional keyword arguments are passed through to `write_error`.
"""
if self._headers_written:
logging.error("Cannot send error response after headers written")
gen_log.error("Cannot send error response after headers written")
if not self._finished:
self.finish()
return
self.clear()
self.set_status(status_code)
reason = None
if 'exc_info' in kwargs:
exception = kwargs['exc_info'][1]
if isinstance(exception, HTTPError) and exception.reason:
reason = exception.reason
self.set_status(status_code, reason=reason)
try:
self.write_error(status_code, **kwargs)
except Exception:
logging.error("Uncaught exception in write_error", exc_info=True)
app_log.error("Uncaught exception in write_error", exc_info=True)
if not self._finished:
self.finish()
@@ -734,10 +778,11 @@ class RequestHandler(object):
``write_error`` may call `write`, `render`, `set_header`, etc
to produce output as usual.
If this error was caused by an uncaught exception, an ``exc_info``
triple will be available as ``kwargs["exc_info"]``. Note that this
exception may not be the "current" exception for purposes of
methods like ``sys.exc_info()`` or ``traceback.format_exc``.
If this error was caused by an uncaught exception (including
HTTPError), an ``exc_info`` triple will be available as
``kwargs["exc_info"]``. Note that this exception may not be
the "current" exception for purposes of methods like
``sys.exc_info()`` or ``traceback.format_exc``.
For historical reasons, if a method ``get_error_html`` exists,
it will be used instead of the default ``write_error`` implementation.
@@ -768,7 +813,7 @@ class RequestHandler(object):
self.finish("<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>" % {
"code": status_code,
"message": httplib.responses[status_code],
"message": self._reason,
})
@property
@@ -964,7 +1009,7 @@ class RequestHandler(object):
return callback(*args, **kwargs)
except Exception, e:
if self._headers_written:
logging.error("Exception after headers written",
app_log.error("Exception after headers written",
exc_info=True)
else:
self._handle_request_exception(e)
@@ -1008,6 +1053,9 @@ class RequestHandler(object):
try:
if self.request.method not in self.SUPPORTED_METHODS:
raise HTTPError(405)
self.path_args = [self.decode_argument(arg) for arg in args]
self.path_kwargs = dict((k, self.decode_argument(v, name=k))
for (k, v) in kwargs.iteritems())
# If XSRF cookies are turned on, reject form submissions without
# the proper cookie
if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
@@ -1015,19 +1063,18 @@ class RequestHandler(object):
self.check_xsrf_cookie()
self.prepare()
if not self._finished:
args = [self.decode_argument(arg) for arg in args]
kwargs = dict((k, self.decode_argument(v, name=k))
for (k, v) in kwargs.iteritems())
getattr(self, self.request.method.lower())(*args, **kwargs)
getattr(self, self.request.method.lower())(
*self.path_args, **self.path_kwargs)
if self._auto_finish and not self._finished:
self.finish()
except Exception, e:
self._handle_request_exception(e)
def _generate_headers(self):
reason = self._reason
lines = [utf8(self.request.version + " " +
str(self._status_code) +
" " + httplib.responses[self._status_code])]
" " + reason)]
lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in
itertools.chain(self._headers.iteritems(), self._list_headers)])
if hasattr(self, "_new_cookie"):
@@ -1053,14 +1100,14 @@ class RequestHandler(object):
if e.log_message:
format = "%d %s: " + e.log_message
args = [e.status_code, self._request_summary()] + list(e.args)
logging.warning(format, *args)
if e.status_code not in httplib.responses:
logging.error("Bad HTTP status code: %d", e.status_code)
gen_log.warning(format, *args)
if e.status_code not in httplib.responses and not e.reason:
gen_log.error("Bad HTTP status code: %d", e.status_code)
self.send_error(500, exc_info=sys.exc_info())
else:
self.send_error(e.status_code, exc_info=sys.exc_info())
else:
logging.error("Uncaught exception %s\n%r", self._request_summary(),
app_log.error("Uncaught exception %s\n%r", self._request_summary(),
self.request, exc_info=True)
self.send_error(500, exc_info=sys.exc_info())
@@ -1205,18 +1252,6 @@ class Application(object):
and we will serve /favicon.ico and /robots.txt from the same directory.
A custom subclass of StaticFileHandler can be specified with the
static_handler_class setting.
.. attribute:: settings
Additional keyword arguments passed to the constructor are saved in the
`settings` dictionary, and are often referred to in documentation as
"application settings".
.. attribute:: debug
If `True` the application runs in debug mode, described in
:ref:`debug-mode`. This is an application setting in the `settings`
dictionary, so handlers can access it.
"""
def __init__(self, handlers=None, default_host="", transforms=None,
wsgi=False, **settings):
@@ -1319,7 +1354,7 @@ class Application(object):
handlers.append(spec)
if spec.name:
if spec.name in self.named_handlers:
logging.warning(
app_log.warning(
"Multiple handlers named %s; replacing previous value",
spec.name)
self.named_handlers[spec.name] = spec
@@ -1443,26 +1478,40 @@ class Application(object):
self.settings["log_function"](handler)
return
if handler.get_status() < 400:
log_method = logging.info
log_method = access_log.info
elif handler.get_status() < 500:
log_method = logging.warning
log_method = access_log.warning
else:
log_method = logging.error
log_method = access_log.error
request_time = 1000.0 * handler.request.request_time()
log_method("%d %s %.2fms", handler.get_status(),
handler._request_summary(), request_time)
class HTTPError(Exception):
"""An exception that will turn into an HTTP error response."""
def __init__(self, status_code, log_message=None, *args):
"""An exception that will turn into an HTTP error response.
:arg int status_code: HTTP status code. Must be listed in
`httplib.responses` unless the ``reason`` keyword argument is given.
:arg string log_message: Message to be written to the log for this error
(will not be shown to the user unless the `Application` is in debug
mode). May contain ``%s``-style placeholders, which will be filled
in with remaining positional parameters.
:arg string reason: Keyword-only argument. The HTTP "reason" phrase
to pass in the status line along with ``status_code``. Normally
determined automatically from ``status_code``, but can be used
to use a non-standard numeric code.
"""
def __init__(self, status_code, log_message=None, *args, **kwargs):
self.status_code = status_code
self.log_message = log_message
self.args = args
self.reason = kwargs.get('reason', None)
def __str__(self):
message = "HTTP %d: %s" % (
self.status_code, httplib.responses[self.status_code])
self.status_code,
self.reason or httplib.responses.get(self.status_code, 'Unknown'))
if self.log_message:
return message + " (" + (self.log_message % self.args) + ")"
else:
@@ -1477,6 +1526,12 @@ class ErrorHandler(RequestHandler):
def prepare(self):
raise HTTPError(self._status_code)
def check_xsrf_cookie(self):
# POSTs to an ErrorHandler don't actually have side effects,
# so we don't need to check the xsrf token. This allows POSTs
# to the wrong url to return a 404 instead of 403.
pass
class RedirectHandler(RequestHandler):
"""Redirects the client to the given URL for all GET requests.
@@ -1563,11 +1618,9 @@ class StaticFileHandler(RequestHandler):
cache_time = self.get_cache_time(path, modified, mime_type)
if cache_time > 0:
self.set_header("Expires", datetime.datetime.utcnow() + \
self.set_header("Expires", datetime.datetime.utcnow() +
datetime.timedelta(seconds=cache_time))
self.set_header("Cache-Control", "max-age=" + str(cache_time))
else:
self.set_header("Cache-Control", "public")
self.set_extra_headers(path)
@@ -1583,9 +1636,6 @@ class StaticFileHandler(RequestHandler):
with open(abspath, "rb") as file:
data = file.read()
hasher = hashlib.sha1()
hasher.update(data)
self.set_header("Etag", '"%s"' % hasher.hexdigest())
if include_body:
self.write(data)
else:
@@ -1646,7 +1696,7 @@ class StaticFileHandler(RequestHandler):
hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
f.close()
except Exception:
logging.error("Could not open static file %r", path)
gen_log.error("Could not open static file %r", path)
hashes[abs_path] = None
hsh = hashes.get(abs_path)
if hsh:
@@ -1721,6 +1771,10 @@ class GZipContentEncoding(OutputTransform):
"gzip" in request.headers.get("Accept-Encoding", "")
def transform_first_chunk(self, status_code, headers, chunk, finishing):
if 'Vary' in headers:
headers['Vary'] += b(', Accept-Encoding')
else:
headers['Vary'] = b('Accept-Encoding')
if self._gzipping:
ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
self._gzipping = (ctype in self.CONTENT_TYPES) and \
@@ -1956,6 +2010,11 @@ class URLSpec(object):
self.name = name
self._path, self._group_count = self._find_groups()
def __repr__(self):
return '%s(%r, %s, kwargs=%r, name=%r)' % \
(self.__class__.__name__, self.regex.pattern,
self.handler_class, self.kwargs, self.name)
def _find_groups(self):
"""Returns a tuple (reverse string, group count) for a url.
@@ -2001,17 +2060,20 @@ class URLSpec(object):
url = URLSpec
def _time_independent_equals(a, b):
if len(a) != len(b):
return False
result = 0
if type(a[0]) is int: # python3 byte strings
for x, y in zip(a, b):
result |= x ^ y
else: # python2
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0
if hasattr(hmac, 'compare_digest'): # python 3.3
_time_independent_equals = hmac.compare_digest
else:
def _time_independent_equals(a, b):
if len(a) != len(b):
return False
result = 0
if type(a[0]) is int: # python3 byte strings
for x, y in zip(a, b):
result |= x ^ y
else: # python2
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0
def create_signed_value(secret, name, value):
@@ -2030,11 +2092,11 @@ def decode_signed_value(secret, name, value, max_age_days=31):
return None
signature = _create_signature(secret, name, parts[0], parts[1])
if not _time_independent_equals(parts[2], signature):
logging.warning("Invalid cookie signature %r", value)
gen_log.warning("Invalid cookie signature %r", value)
return None
timestamp = int(parts[1])
if timestamp < time.time() - max_age_days * 86400:
logging.warning("Expired cookie %r", value)
gen_log.warning("Expired cookie %r", value)
return None
if timestamp > time.time() + 31 * 86400:
# _cookie_signature does not hash a delimiter between the
@@ -2042,10 +2104,10 @@ def decode_signed_value(secret, name, value, max_age_days=31):
# digits from the payload to the timestamp without altering the
# signature. For backwards compatibility, sanity-check timestamp
# here instead of modifying _cookie_signature.
logging.warning("Cookie timestamp in future; possible tampering %r", value)
gen_log.warning("Cookie timestamp in future; possible tampering %r", value)
return None
if parts[1].startswith(b("0")):
logging.warning("Tampered cookie %r", value)
gen_log.warning("Tampered cookie %r", value)
return None
try:
return base64.b64decode(parts[0])