Tornado update

This commit is contained in:
Ruud
2013-02-22 23:20:16 +01:00
parent 3eed34c710
commit a7b78d4131
36 changed files with 1784 additions and 1293 deletions
+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.
"""
from __future__ import absolute_import, division, with_statement
from __future__ import absolute_import, division, print_function, with_statement
import Cookie
import base64
import binascii
import calendar
import datetime
import email.utils
import functools
import gzip
import hashlib
import hmac
import httplib
import itertools
import mimetypes
import numbers
import os.path
import re
import stat
@@ -73,23 +70,37 @@ import time
import tornado
import traceback
import types
import urllib
import urlparse
import uuid
from tornado import escape
from tornado import httputil
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
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:
from io import BytesIO # python 3
except ImportError:
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):
"""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_loader_lock = threading.Lock()
_remove_control_chars_regex = re.compile(r"[\x00-\x08\x0e-\x1f]")
def __init__(self, application, request, **kwargs):
super(RequestHandler, self).__init__()
@@ -116,19 +128,19 @@ class RequestHandler(object):
self.path_args = None
self.path_kwargs = None
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
# template namespace. Historically only `modules` was available
# but could be clobbered by user additions to the namespace.
# The template {% module %} directive looks in `_modules` to avoid
# possible conflicts.
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.clear()
# Check since connection is not available in WSGI
if getattr(self.request, "connection", None):
self.request.connection.stream.set_close_callback(
self.request.connection.set_close_callback(
self.on_connection_close)
self.initialize(**kwargs)
@@ -215,25 +227,18 @@ class RequestHandler(object):
def clear(self):
"""Resets all headers and content for this response."""
# The performance cost of tornado.httputil.HTTPHeaders is significant
# (slowing down a benchmark with a trivial handler by more than 10%),
# and its case-normalization is not generally necessary for
# headers we generate on the server side, so use a plain dict
# 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._headers = httputil.HTTPHeaders({
"Server": "TornadoServer/%s" % tornado.version,
"Content-Type": "text/html; charset=UTF-8",
"Date": httputil.format_timestamp(time.gmtime()),
})
self.set_default_headers()
if not self.request.supports_http_1_1():
if self.request.headers.get("Connection") == "Keep-Alive":
self.set_header("Connection", "Keep-Alive")
self._write_buffer = []
self._status_code = 200
self._reason = httplib.responses[200]
self._reason = httputil.responses[200]
def set_default_headers(self):
"""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)
else:
try:
self._reason = httplib.responses[status_code]
self._reason = httputil.responses[status_code]
except KeyError:
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
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):
"""Clears an outgoing header, undoing a previous `set_header` call.
@@ -295,20 +300,19 @@ class RequestHandler(object):
def _convert_header_value(self, value):
if isinstance(value, bytes_type):
pass
elif isinstance(value, unicode):
elif isinstance(value, unicode_type):
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 str(value)
elif isinstance(value, datetime.datetime):
t = calendar.timegm(value.utctimetuple())
return email.utils.formatdate(t, localtime=False, usegmt=True)
return httputil.format_timestamp(value)
else:
raise TypeError("Unsupported header value %r" % value)
# If \n is allowed into the header, it is possible to inject
# additional headers or split the request. Also cap length to
# 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)
return value
@@ -339,13 +343,14 @@ class RequestHandler(object):
The returned values are always unicode.
"""
values = []
for v in self.request.arguments.get(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
# 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:
v = v.strip()
values.append(v)
@@ -403,12 +408,10 @@ class RequestHandler(object):
expires = datetime.datetime.utcnow() + datetime.timedelta(
days=expires_days)
if expires:
timestamp = calendar.timegm(expires.utctimetuple())
morsel["expires"] = email.utils.formatdate(
timestamp, localtime=False, usegmt=True)
morsel["expires"] = httputil.format_timestamp(expires)
if path:
morsel["path"] = path
for k, v in kwargs.iteritems():
for k, v in kwargs.items():
if k == 'max_age':
k = 'max-age'
morsel[k] = v
@@ -421,7 +424,7 @@ class RequestHandler(object):
def clear_all_cookies(self):
"""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)
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
self.set_status(status)
# 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),
url))
self.finish()
@@ -523,13 +526,13 @@ class RequestHandler(object):
css_files = []
html_heads = []
html_bodies = []
for module in getattr(self, "_active_modules", {}).itervalues():
for module in getattr(self, "_active_modules", {}).values():
embed_part = module.embedded_javascript()
if embed_part:
js_embed.append(utf8(embed_part))
file_part = module.javascript_files()
if file_part:
if isinstance(file_part, (unicode, bytes_type)):
if isinstance(file_part, (unicode_type, bytes_type)):
js_files.append(file_part)
else:
js_files.extend(file_part)
@@ -538,7 +541,7 @@ class RequestHandler(object):
css_embed.append(utf8(embed_part))
file_part = module.css_files()
if file_part:
if isinstance(file_part, (unicode, bytes_type)):
if isinstance(file_part, (unicode_type, bytes_type)):
css_files.append(file_part)
else:
css_files.extend(file_part)
@@ -564,13 +567,13 @@ class RequestHandler(object):
js = ''.join('<script src="' + escape.xhtml_escape(p) +
'" type="text/javascript"></script>'
for p in paths)
sloc = html.rindex(b('</body>'))
html = html[:sloc] + utf8(js) + b('\n') + html[sloc:]
sloc = html.rindex(b'</body>')
html = html[:sloc] + utf8(js) + b'\n' + html[sloc:]
if js_embed:
js = b('<script type="text/javascript">\n//<![CDATA[\n') + \
b('\n').join(js_embed) + b('\n//]]>\n</script>')
sloc = html.rindex(b('</body>'))
html = html[:sloc] + js + b('\n') + html[sloc:]
js = b'<script type="text/javascript">\n//<![CDATA[\n' + \
b'\n'.join(js_embed) + b'\n//]]>\n</script>'
sloc = html.rindex(b'</body>')
html = html[:sloc] + js + b'\n' + html[sloc:]
if css_files:
paths = []
unique_paths = set()
@@ -583,19 +586,19 @@ class RequestHandler(object):
css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
'type="text/css" rel="stylesheet"/>'
for p in paths)
hloc = html.index(b('</head>'))
html = html[:hloc] + utf8(css) + b('\n') + html[hloc:]
hloc = html.index(b'</head>')
html = html[:hloc] + utf8(css) + b'\n' + html[hloc:]
if css_embed:
css = b('<style type="text/css">\n') + b('\n').join(css_embed) + \
b('\n</style>')
hloc = html.index(b('</head>'))
html = html[:hloc] + css + b('\n') + html[hloc:]
css = b'<style type="text/css">\n' + b'\n'.join(css_embed) + \
b'\n</style>'
hloc = html.index(b'</head>')
html = html[:hloc] + css + b'\n' + html[hloc:]
if html_heads:
hloc = html.index(b('</head>'))
html = html[:hloc] + b('').join(html_heads) + b('\n') + html[hloc:]
hloc = html.index(b'</head>')
html = html[:hloc] + b''.join(html_heads) + b'\n' + html[hloc:]
if html_bodies:
hloc = html.index(b('</body>'))
html = html[:hloc] + b('').join(html_bodies) + b('\n') + html[hloc:]
hloc = html.index(b'</body>')
html = html[:hloc] + b''.join(html_bodies) + b'\n' + html[hloc:]
self.finish(html)
def render_string(self, template_name, **kwargs):
@@ -675,19 +678,19 @@ class RequestHandler(object):
if self.application._wsgi:
raise Exception("WSGI applications do not support flush()")
chunk = b("").join(self._write_buffer)
chunk = b"".join(self._write_buffer)
self._write_buffer = []
if not self._headers_written:
self._headers_written = True
for transform in self._transforms:
self._status_code, self._headers, 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()
else:
for transform in self._transforms:
chunk = transform.transform_chunk(chunk, include_footers)
headers = b("")
headers = b""
# Ignore the chunk and only write the headers for HEAD requests
if self.request.method == "HEAD":
@@ -712,7 +715,7 @@ class RequestHandler(object):
if not self._headers_written:
if (self._status_code == 200 and
self.request.method in ("GET", "HEAD") and
"Etag" not in self._headers):
"Etag" not in self._headers):
etag = self.compute_etag()
if etag is not None:
self.set_header("Etag", etag)
@@ -812,9 +815,9 @@ class RequestHandler(object):
else:
self.finish("<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>" % {
"code": status_code,
"message": self._reason,
})
"code": status_code,
"message": self._reason,
})
@property
def locale(self):
@@ -861,7 +864,7 @@ class RequestHandler(object):
score = 1.0
locales.append((parts[0], score))
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]
return locale.get(*codes)
return locale.get(default)
@@ -1007,7 +1010,7 @@ class RequestHandler(object):
def wrapper(*args, **kwargs):
try:
return callback(*args, **kwargs)
except Exception, e:
except Exception as e:
if self._headers_written:
app_log.error("Exception after headers written",
exc_info=True)
@@ -1055,11 +1058,11 @@ class RequestHandler(object):
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())
for (k, v) in kwargs.items())
# If XSRF cookies are turned on, reject form submissions without
# the proper cookie
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.prepare()
if not self._finished:
@@ -1067,7 +1070,7 @@ class RequestHandler(object):
*self.path_args, **self.path_kwargs)
if self._auto_finish and not self._finished:
self.finish()
except Exception, e:
except Exception as e:
self._handle_request_exception(e)
def _generate_headers(self):
@@ -1075,12 +1078,12 @@ class RequestHandler(object):
lines = [utf8(self.request.version + " " +
str(self._status_code) +
" " + reason)]
lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in
itertools.chain(self._headers.iteritems(), self._list_headers)])
lines.extend([utf8(n) + b": " + utf8(v) for n, v in self._headers.get_all()])
if hasattr(self, "_new_cookie"):
for cookie in self._new_cookie.values():
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):
"""Logs the current request.
@@ -1101,7 +1104,7 @@ class RequestHandler(object):
format = "%d %s: " + e.log_message
args = [e.status_code, self._request_summary()] + list(e.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)
self.send_error(500, exc_info=sys.exc_info())
else:
@@ -1161,7 +1164,7 @@ def asynchronous(method):
raise Exception("@asynchronous is not supported for WSGI apps")
self._auto_finish = False
with stack_context.ExceptionStackContext(
self._stack_context_handle_exception):
self._stack_context_handle_exception):
return method(self, *args, **kwargs)
return wrapper
@@ -1334,7 +1337,7 @@ class Application(object):
self.handlers.append((re.compile(host_pattern), handlers))
for spec in host_handlers:
if type(spec) is type(()):
if isinstance(spec, type(())):
assert len(spec) in (2, 3)
pattern = spec[0]
handler = spec[1]
@@ -1375,20 +1378,20 @@ class Application(object):
return matches or None
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))
for n in dir(methods)))
elif isinstance(methods, list):
for m in methods:
self._load_ui_methods(m)
else:
for name, fn in methods.iteritems():
for name, fn in methods.items():
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
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))
for n in dir(modules)))
elif isinstance(modules, list):
@@ -1396,7 +1399,7 @@ class Application(object):
self._load_ui_modules(m)
else:
assert isinstance(modules, dict)
for name, cls in modules.iteritems():
for name, cls in modules.items():
try:
if issubclass(cls, UIModule):
self.ui_modules[name] = cls
@@ -1434,7 +1437,7 @@ class Application(object):
if spec.regex.groupindex:
kwargs = dict(
(str(k), unquote(v))
for (k, v) in match.groupdict().iteritems())
for (k, v) in match.groupdict().items())
else:
args = [unquote(s) for s in match.groups()]
break
@@ -1510,7 +1513,7 @@ class HTTPError(Exception):
def __str__(self):
message = "HTTP %d: %s" % (
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:
return message + " (" + (self.log_message % self.args) + ")"
else:
@@ -1618,7 +1621,7 @@ class StaticFileHandler(RequestHandler):
if cache_time > 0:
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_extra_headers(path)
@@ -1771,9 +1774,9 @@ class GZipContentEncoding(OutputTransform):
def transform_first_chunk(self, status_code, headers, chunk, finishing):
if 'Vary' in headers:
headers['Vary'] += b(', Accept-Encoding')
headers['Vary'] += b', Accept-Encoding'
else:
headers['Vary'] = b('Accept-Encoding')
headers['Vary'] = b'Accept-Encoding'
if self._gzipping:
ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
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
# with chunked encoding
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:
block += b("0\r\n\r\n")
block += b"0\r\n\r\n"
return block
@@ -1846,7 +1849,7 @@ def authenticated(method):
next_url = self.request.full_url()
else:
next_url = self.request.uri
url += "?" + urllib.urlencode(dict(next=next_url))
url += "?" + urlencode(dict(next=next_url))
self.redirect(url)
return
raise HTTPError(403)
@@ -1954,7 +1957,7 @@ class TemplateModule(UIModule):
def javascript_files(self):
result = []
for f in self._get_resources("javascript_files"):
if isinstance(f, (unicode, bytes_type)):
if isinstance(f, (unicode_type, bytes_type)):
result.append(f)
else:
result.extend(f)
@@ -1966,7 +1969,7 @@ class TemplateModule(UIModule):
def css_files(self):
result = []
for f in self._get_resources("css_files"):
if isinstance(f, (unicode, bytes_type)):
if isinstance(f, (unicode_type, bytes_type)):
result.append(f)
else:
result.extend(f)
@@ -2011,8 +2014,8 @@ class URLSpec(object):
def __repr__(self):
return '%s(%r, %s, kwargs=%r, name=%r)' % \
(self.__class__.__name__, self.regex.pattern,
self.handler_class, self.kwargs, self.name)
(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.
@@ -2051,7 +2054,7 @@ class URLSpec(object):
return self._path
converted_args = []
for a in args:
if not isinstance(a, (unicode, bytes_type)):
if not isinstance(a, (unicode_type, bytes_type)):
a = str(a)
converted_args.append(escape.url_escape(utf8(a)))
return self._path % tuple(converted_args)
@@ -2066,7 +2069,7 @@ else:
if len(a) != len(b):
return False
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):
result |= x ^ y
else: # python2
@@ -2079,14 +2082,14 @@ def create_signed_value(secret, name, value):
timestamp = utf8(str(int(time.time())))
value = base64.b64encode(utf8(value))
signature = _create_signature(secret, name, value, timestamp)
value = b("|").join([value, timestamp, signature])
value = b"|".join([value, timestamp, signature])
return value
def decode_signed_value(secret, name, value, max_age_days=31):
if not value:
return None
parts = utf8(value).split(b("|"))
parts = utf8(value).split(b"|")
if len(parts) != 3:
return None
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.
gen_log.warning("Cookie timestamp in future; possible tampering %r", value)
return None
if parts[1].startswith(b("0")):
if parts[1].startswith(b"0"):
gen_log.warning("Tampered cookie %r", value)
return None
try: