3
# Copyright 2009 Facebook
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6
# not use this file except in compliance with the License. You may obtain
7
# a copy of the License at
9
# http://www.apache.org/licenses/LICENSE-2.0
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations
17
"""The Tornado web framework.
19
The Tornado web framework looks a bit like web.py (http://webpy.org/) or
20
Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
21
but with additional tools and optimizations to take advantage of the
22
Tornado non-blocking web server and tools.
24
Here is the canonical "Hello, world" example app:
26
import tornado.httpserver
30
class MainHandler(tornado.web.RequestHandler):
32
self.write("Hello, world")
34
if __name__ == "__main__":
35
application = tornado.web.Application([
38
http_server = tornado.httpserver.HTTPServer(application)
39
http_server.listen(8888)
40
tornado.ioloop.IOLoop.instance().start()
42
See the Tornado walkthrough on GitHub for more details and a good
43
getting started guide.
73
_log = logging.getLogger('tornado.web')
75
class RequestHandler(object):
76
"""Subclass this class and define get() or post() to make a handler.
78
If you want to support more methods than the standard GET/HEAD/POST, you
79
should override the class variable SUPPORTED_METHODS in your
82
SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT")
84
def __init__(self, application, request, transforms=None):
85
self.application = application
86
self.request = request
87
self._headers_written = False
88
self._finished = False
89
self._auto_finish = True
90
self._transforms = transforms or []
91
self.ui = _O((n, self._ui_method(m)) for n, m in
92
application.ui_methods.iteritems())
93
self.ui["modules"] = _O((n, self._ui_module(n, m)) for n, m in
94
application.ui_modules.iteritems())
96
# Check since connection is not available in WSGI
97
if hasattr(self.request, "connection"):
98
self.request.connection.stream.set_close_callback(
99
self.on_connection_close)
103
return self.application.settings
105
def head(self, *args, **kwargs):
108
def get(self, *args, **kwargs):
111
def post(self, *args, **kwargs):
114
def delete(self, *args, **kwargs):
117
def put(self, *args, **kwargs):
121
"""Called before the actual handler method.
123
Useful to override in a handler if you want a common bottleneck for
124
all of your requests.
128
def on_connection_close(self):
129
"""Called in async handlers if the client closed the connection.
131
You may override this to clean up resources associated with
132
long-lived connections.
134
Note that the select()-based implementation of IOLoop does not detect
135
closed connections and so this method will not be called until
136
you try (and fail) to produce some output. The epoll- and kqueue-
137
based implementations should detect closed connections even while
143
"""Resets all headers and content for this response."""
145
"Server": "TornadoServer/0.1",
146
"Content-Type": "text/html; charset=UTF-8",
148
if not self.request.supports_http_1_1():
149
if self.request.headers.get("Connection") == "Keep-Alive":
150
self.set_header("Connection", "Keep-Alive")
151
self._write_buffer = []
152
self._status_code = 200
154
def set_status(self, status_code):
155
"""Sets the status code for our response."""
156
assert status_code in httplib.responses
157
self._status_code = status_code
159
def set_header(self, name, value):
160
"""Sets the given response header name and value.
162
If a datetime is given, we automatically format it according to the
163
HTTP specification. If the value is not a string, we convert it to
164
a string. All header values are then encoded as UTF-8.
166
if isinstance(value, datetime.datetime):
167
t = calendar.timegm(value.utctimetuple())
168
value = email.utils.formatdate(t, localtime=False, usegmt=True)
169
elif isinstance(value, int) or isinstance(value, long):
173
# If \n is allowed into the header, it is possible to inject
174
# additional headers or split the request. Also cap length to
175
# prevent obviously erroneous values.
176
safe_value = re.sub(r"[\x00-\x1f]", " ", value)[:4000]
177
if safe_value != value:
178
raise ValueError("Unsafe header value %r", value)
179
self._headers[name] = value
182
def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
183
"""Returns the value of the argument with the given name.
185
If default is not provided, the argument is considered to be
186
required, and we throw an HTTP 404 exception if it is missing.
188
The returned value is always unicode.
190
values = self.request.arguments.get(name, None)
192
if default is self._ARG_DEFAULT:
193
raise HTTPError(404, "Missing argument %s" % name)
195
# Get rid of any weird control chars
196
value = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", values[-1])
197
value = _unicode(value)
198
if strip: value = value.strip()
203
"""A dictionary of Cookie.Morsel objects."""
204
if not hasattr(self, "_cookies"):
205
self._cookies = Cookie.BaseCookie()
206
if "Cookie" in self.request.headers:
208
self._cookies.load(self.request.headers["Cookie"])
210
self.clear_all_cookies()
213
def get_cookie(self, name, default=None):
214
"""Gets the value of the cookie with the given name, else default."""
215
if name in self.cookies:
216
return self.cookies[name].value
219
def set_cookie(self, name, value, domain=None, expires=None, path="/",
221
"""Sets the given cookie name/value with the given options."""
224
if re.search(r"[\x00-\x20]", name + value):
225
# Don't let us accidentally inject bad stuff
226
raise ValueError("Invalid cookie %r: %r" % (name, value))
227
if not hasattr(self, "_new_cookies"):
228
self._new_cookies = []
229
new_cookie = Cookie.BaseCookie()
230
self._new_cookies.append(new_cookie)
231
new_cookie[name] = value
233
new_cookie[name]["domain"] = domain
234
if expires_days is not None and not expires:
235
expires = datetime.datetime.utcnow() + datetime.timedelta(
238
timestamp = calendar.timegm(expires.utctimetuple())
239
new_cookie[name]["expires"] = email.utils.formatdate(
240
timestamp, localtime=False, usegmt=True)
242
new_cookie[name]["path"] = path
244
def clear_cookie(self, name, path="/", domain=None):
245
"""Deletes the cookie with the given name."""
246
expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
247
self.set_cookie(name, value="", path=path, expires=expires,
250
def clear_all_cookies(self):
251
"""Deletes all the cookies the user sent with this request."""
252
for name in self.cookies.iterkeys():
253
self.clear_cookie(name)
255
def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
256
"""Signs and timestamps a cookie so it cannot be forged.
258
You must specify the 'cookie_secret' setting in your Application
259
to use this method. It should be a long, random sequence of bytes
260
to be used as the HMAC secret for the signature.
262
To read a cookie set with this method, use get_secure_cookie().
264
timestamp = str(int(time.time()))
265
value = base64.b64encode(value)
266
signature = self._cookie_signature(name, value, timestamp)
267
value = "|".join([value, timestamp, signature])
268
self.set_cookie(name, value, expires_days=expires_days, **kwargs)
270
def get_secure_cookie(self, name, include_name=True, value=None):
271
"""Returns the given signed cookie if it validates, or None.
273
In older versions of Tornado (0.1 and 0.2), we did not include the
274
name of the cookie in the cookie signature. To read these old-style
275
cookies, pass include_name=False to this method. Otherwise, all
276
attempts to read old-style cookies will fail (and you may log all
277
your users out whose cookies were written with a previous Tornado
280
if value is None: value = self.get_cookie(name)
281
if not value: return None
282
parts = value.split("|")
283
if len(parts) != 3: return None
285
signature = self._cookie_signature(name, parts[0], parts[1])
287
signature = self._cookie_signature(parts[0], parts[1])
288
if not _time_independent_equals(parts[2], signature):
289
_log.warning("Invalid cookie signature %r", value)
291
timestamp = int(parts[1])
292
if timestamp < time.time() - 31 * 86400:
293
_log.warning("Expired cookie %r", value)
296
return base64.b64decode(parts[0])
300
def _cookie_signature(self, *parts):
301
self.require_setting("cookie_secret", "secure cookies")
302
hash = hmac.new(self.application.settings["cookie_secret"],
303
digestmod=hashlib.sha1)
304
for part in parts: hash.update(part)
305
return hash.hexdigest()
307
def redirect(self, url, permanent=False):
308
"""Sends a redirect to the given (optionally relative) URL."""
309
if self._headers_written:
310
raise Exception("Cannot redirect after headers have been written")
311
self.set_status(301 if permanent else 302)
313
url = re.sub(r"[\x00-\x20]+", "", _utf8(url))
314
self.set_header("Location", urlparse.urljoin(self.request.uri, url))
317
def write(self, chunk):
318
"""Writes the given chunk to the output buffer.
320
To write the output to the network, use the flush() method below.
322
If the given chunk is a dictionary, we write it as JSON and set
323
the Content-Type of the response to be text/javascript.
325
assert not self._finished
326
if isinstance(chunk, dict):
327
chunk = escape.json_encode(chunk)
328
self.set_header("Content-Type", "text/javascript; charset=UTF-8")
330
self._write_buffer.append(chunk)
332
def render(self, template_name, **kwargs):
333
"""Renders the template with the given arguments as the response."""
334
html = self.render_string(template_name, **kwargs)
336
# Insert the additional JS and CSS added by the modules on the page
343
for module in getattr(self, "_active_modules", {}).itervalues():
344
embed_part = module.embedded_javascript()
345
if embed_part: js_embed.append(_utf8(embed_part))
346
file_part = module.javascript_files()
348
if isinstance(file_part, basestring):
349
js_files.append(file_part)
351
js_files.extend(file_part)
352
embed_part = module.embedded_css()
353
if embed_part: css_embed.append(_utf8(embed_part))
354
file_part = module.css_files()
356
if isinstance(file_part, basestring):
357
css_files.append(file_part)
359
css_files.extend(file_part)
360
head_part = module.html_head()
361
if head_part: html_heads.append(_utf8(head_part))
362
body_part = module.html_body()
363
if body_part: html_bodies.append(_utf8(body_part))
365
# Maintain order of JavaScript files given by modules
368
for path in js_files:
369
if not path.startswith("/") and not path.startswith("http:"):
370
path = self.static_url(path)
371
if path not in unique_paths:
373
unique_paths.add(path)
374
js = ''.join('<script src="' + escape.xhtml_escape(p) +
375
'" type="text/javascript"></script>'
377
sloc = html.rindex('</body>')
378
html = html[:sloc] + js + '\n' + html[sloc:]
380
js = '<script type="text/javascript">\n//<![CDATA[\n' + \
381
'\n'.join(js_embed) + '\n//]]>\n</script>'
382
sloc = html.rindex('</body>')
383
html = html[:sloc] + js + '\n' + html[sloc:]
386
for path in css_files:
387
if not path.startswith("/") and not path.startswith("http:"):
388
paths.add(self.static_url(path))
391
css = ''.join('<link href="' + escape.xhtml_escape(p) + '" '
392
'type="text/css" rel="stylesheet"/>'
394
hloc = html.index('</head>')
395
html = html[:hloc] + css + '\n' + html[hloc:]
397
css = '<style type="text/css">\n' + '\n'.join(css_embed) + \
399
hloc = html.index('</head>')
400
html = html[:hloc] + css + '\n' + html[hloc:]
402
hloc = html.index('</head>')
403
html = html[:hloc] + ''.join(html_heads) + '\n' + html[hloc:]
405
hloc = html.index('</body>')
406
html = html[:hloc] + ''.join(html_bodies) + '\n' + html[hloc:]
409
def render_string(self, template_name, **kwargs):
410
"""Generate the given template with the given arguments.
412
We return the generated string. To generate and write a template
413
as a response, use render() above.
415
# If no template_path is specified, use the path of the calling file
416
template_path = self.application.settings.get("template_path")
417
if not template_path:
418
frame = sys._getframe(0)
419
web_file = frame.f_code.co_filename
420
while frame.f_code.co_filename == web_file:
422
template_path = os.path.dirname(frame.f_code.co_filename)
423
if not getattr(RequestHandler, "_templates", None):
424
RequestHandler._templates = {}
425
if template_path not in RequestHandler._templates:
426
loader = self.application.settings.get("template_loader") or\
427
template.Loader(template_path)
428
RequestHandler._templates[template_path] = loader
429
t = RequestHandler._templates[template_path].load(template_name)
432
request=self.request,
433
current_user=self.current_user,
435
_=self.locale.translate,
436
static_url=self.static_url,
437
xsrf_form_html=self.xsrf_form_html,
438
reverse_url=self.application.reverse_url
442
return t.generate(**args)
444
def flush(self, include_footers=False):
445
"""Flushes the current output buffer to the nextwork."""
446
if self.application._wsgi:
447
raise Exception("WSGI applications do not support flush()")
449
chunk = "".join(self._write_buffer)
450
self._write_buffer = []
451
if not self._headers_written:
452
self._headers_written = True
453
for transform in self._transforms:
454
self._headers, chunk = transform.transform_first_chunk(
455
self._headers, chunk, include_footers)
456
headers = self._generate_headers()
458
for transform in self._transforms:
459
chunk = transform.transform_chunk(chunk, include_footers)
462
# Ignore the chunk and only write the headers for HEAD requests
463
if self.request.method == "HEAD":
464
if headers: self.request.write(headers)
468
self.request.write(headers + chunk)
470
def finish(self, chunk=None):
471
"""Finishes this response, ending the HTTP request."""
472
assert not self._finished
473
if chunk is not None: self.write(chunk)
475
# Automatically support ETags and add the Content-Length header if
476
# we have not flushed any content yet.
477
if not self._headers_written:
478
if (self._status_code == 200 and self.request.method == "GET" and
479
"Etag" not in self._headers):
480
hasher = hashlib.sha1()
481
for part in self._write_buffer:
483
etag = '"%s"' % hasher.hexdigest()
484
inm = self.request.headers.get("If-None-Match")
485
if inm and inm.find(etag) != -1:
486
self._write_buffer = []
489
self.set_header("Etag", etag)
490
if "Content-Length" not in self._headers:
491
content_length = sum(len(part) for part in self._write_buffer)
492
self.set_header("Content-Length", content_length)
494
if not self.application._wsgi:
495
self.flush(include_footers=True)
496
self.request.finish()
498
self._finished = True
500
def send_error(self, status_code=500, **kwargs):
501
"""Sends the given HTTP error code to the browser.
503
We also send the error HTML for the given error code as returned by
504
get_error_html. Override that method if you want custom error pages
505
for your application.
507
if self._headers_written:
508
_log.error("Cannot send error response after headers written")
509
if not self._finished:
513
self.set_status(status_code)
514
message = self.get_error_html(status_code, **kwargs)
517
def get_error_html(self, status_code, **kwargs):
518
"""Override to implement custom error pages.
520
If this error was caused by an uncaught exception, the
521
exception object can be found in kwargs e.g. kwargs['exception']
523
return "<html><title>%(code)d: %(message)s</title>" \
524
"<body>%(code)d: %(message)s</body></html>" % {
526
"message": httplib.responses[status_code],
531
"""The local for the current session.
533
Determined by either get_user_locale, which you can override to
534
set the locale based on, e.g., a user preference stored in a
535
database, or get_browser_locale, which uses the Accept-Language
538
if not hasattr(self, "_locale"):
539
self._locale = self.get_user_locale()
541
self._locale = self.get_browser_locale()
545
def get_user_locale(self):
546
"""Override to determine the locale from the authenticated user.
548
If None is returned, we use the Accept-Language header.
552
def get_browser_locale(self, default="en_US"):
553
"""Determines the user's locale from Accept-Language header.
555
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
557
if "Accept-Language" in self.request.headers:
558
languages = self.request.headers["Accept-Language"].split(",")
560
for language in languages:
561
parts = language.strip().split(";")
562
if len(parts) > 1 and parts[1].startswith("q="):
564
score = float(parts[1][2:])
565
except (ValueError, TypeError):
569
locales.append((parts[0], score))
571
locales.sort(key=lambda (l, s): s, reverse=True)
572
codes = [l[0] for l in locales]
573
return locale.get(*codes)
574
return locale.get(default)
577
def current_user(self):
578
"""The authenticated user for this request.
580
Determined by either get_current_user, which you can override to
581
set the user based on, e.g., a cookie. If that method is not
582
overridden, this method always returns None.
584
We lazy-load the current user the first time this method is called
585
and cache the result after that.
587
if not hasattr(self, "_current_user"):
588
self._current_user = self.get_current_user()
589
return self._current_user
591
def get_current_user(self):
592
"""Override to determine the current user from, e.g., a cookie."""
595
def get_login_url(self):
596
"""Override to customize the login URL based on the request.
598
By default, we use the 'login_url' application setting.
600
self.require_setting("login_url", "@tornado.web.authenticated")
601
return self.application.settings["login_url"]
604
def xsrf_token(self):
605
"""The XSRF-prevention token for the current user/session.
607
To prevent cross-site request forgery, we set an '_xsrf' cookie
608
and include the same '_xsrf' value as an argument with all POST
609
requests. If the two do not match, we reject the form submission
610
as a potential forgery.
612
See http://en.wikipedia.org/wiki/Cross-site_request_forgery
614
if not hasattr(self, "_xsrf_token"):
615
token = self.get_cookie("_xsrf")
617
token = binascii.b2a_hex(uuid.uuid4().bytes)
618
expires_days = 30 if self.current_user else None
619
self.set_cookie("_xsrf", token, expires_days=expires_days)
620
self._xsrf_token = token
621
return self._xsrf_token
623
def check_xsrf_cookie(self):
624
"""Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
626
To prevent cross-site request forgery, we set an '_xsrf' cookie
627
and include the same '_xsrf' value as an argument with all POST
628
requests. If the two do not match, we reject the form submission
629
as a potential forgery.
631
See http://en.wikipedia.org/wiki/Cross-site_request_forgery
633
if self.request.headers.get("X-Requested-With") == "XMLHttpRequest":
635
token = self.get_argument("_xsrf", None)
637
raise HTTPError(403, "'_xsrf' argument missing from POST")
638
if self.xsrf_token != token:
639
raise HTTPError(403, "XSRF cookie does not match POST argument")
641
def xsrf_form_html(self):
642
"""An HTML <input/> element to be included with all POST forms.
644
It defines the _xsrf input value, which we check on all POST
645
requests to prevent cross-site request forgery. If you have set
646
the 'xsrf_cookies' application setting, you must include this
647
HTML within all of your HTML forms.
649
See check_xsrf_cookie() above for more information.
651
return '<input type="hidden" name="_xsrf" value="' + \
652
escape.xhtml_escape(self.xsrf_token) + '"/>'
654
def static_url(self, path):
655
"""Returns a static URL for the given relative static file path.
657
This method requires you set the 'static_path' setting in your
658
application (which specifies the root directory of your static
661
We append ?v=<signature> to the returned URL, which makes our
662
static file handler set an infinite expiration header on the
663
returned content. The signature is based on the content of the
666
If this handler has a "include_host" attribute, we include the
667
full host for every static URL, including the "http://". Set
668
this attribute for handlers whose output needs non-relative static
671
self.require_setting("static_path", "static_url")
672
if not hasattr(RequestHandler, "_static_hashes"):
673
RequestHandler._static_hashes = {}
674
hashes = RequestHandler._static_hashes
675
if path not in hashes:
677
f = open(os.path.join(
678
self.application.settings["static_path"], path))
679
hashes[path] = hashlib.md5(f.read()).hexdigest()
682
_log.error("Could not open static file %r", path)
684
base = self.request.protocol + "://" + self.request.host \
685
if getattr(self, "include_host", False) else ""
686
static_url_prefix = self.settings.get('static_url_prefix', '/static/')
688
return base + static_url_prefix + path + "?v=" + hashes[path][:5]
690
return base + static_url_prefix + path
692
def async_callback(self, callback, *args, **kwargs):
693
"""Wrap callbacks with this if they are used on asynchronous requests.
695
Catches exceptions and properly finishes the request.
700
callback = functools.partial(callback, *args, **kwargs)
701
def wrapper(*args, **kwargs):
703
return callback(*args, **kwargs)
705
if self._headers_written:
706
_log.error("Exception after headers written",
709
self._handle_request_exception(e)
712
def require_setting(self, name, feature="this feature"):
713
"""Raises an exception if the given app setting is not defined."""
714
if not self.application.settings.get(name):
715
raise Exception("You must define the '%s' setting in your "
716
"application to use %s" % (name, feature))
718
def reverse_url(self, name, *args):
719
return self.application.reverse_url(name, *args)
721
def _execute(self, transforms, *args, **kwargs):
722
"""Executes this request with the given output transforms."""
723
self._transforms = transforms
725
if self.request.method not in self.SUPPORTED_METHODS:
727
# If XSRF cookies are turned on, reject form submissions without
729
if self.request.method == "POST" and \
730
self.application.settings.get("xsrf_cookies"):
731
self.check_xsrf_cookie()
733
if not self._finished:
734
getattr(self, self.request.method.lower())(*args, **kwargs)
735
if self._auto_finish and not self._finished:
738
self._handle_request_exception(e)
740
def _generate_headers(self):
741
lines = [self.request.version + " " + str(self._status_code) + " " +
742
httplib.responses[self._status_code]]
743
lines.extend(["%s: %s" % (n, v) for n, v in self._headers.iteritems()])
744
for cookie_dict in getattr(self, "_new_cookies", []):
745
for cookie in cookie_dict.values():
746
lines.append("Set-Cookie: " + cookie.OutputString(None))
747
return "\r\n".join(lines) + "\r\n\r\n"
750
if self._status_code < 400:
751
log_method = _log.info
752
elif self._status_code < 500:
753
log_method = _log.warning
755
log_method = _log.error
756
request_time = 1000.0 * self.request.request_time()
757
log_method("%d %s %.2fms", self._status_code,
758
self._request_summary(), request_time)
760
def _request_summary(self):
761
return self.request.method + " " + self.request.uri + " (" + \
762
self.request.remote_ip + ")"
764
def _handle_request_exception(self, e):
765
if isinstance(e, HTTPError):
767
format = "%d %s: " + e.log_message
768
args = [e.status_code, self._request_summary()] + list(e.args)
769
_log.warning(format, *args)
770
if e.status_code not in httplib.responses:
771
_log.error("Bad HTTP status code: %d", e.status_code)
772
self.send_error(500, exception=e)
774
self.send_error(e.status_code, exception=e)
776
_log.error("Uncaught exception %s\n%r", self._request_summary(),
777
self.request, exc_info=e)
778
self.send_error(500, exception=e)
780
def _ui_module(self, name, module):
781
def render(*args, **kwargs):
782
if not hasattr(self, "_active_modules"):
783
self._active_modules = {}
784
if name not in self._active_modules:
785
self._active_modules[name] = module(self)
786
rendered = self._active_modules[name].render(*args, **kwargs)
790
def _ui_method(self, method):
791
return lambda *args, **kwargs: method(self, *args, **kwargs)
794
def asynchronous(method):
795
"""Wrap request handler methods with this if they are asynchronous.
797
If this decorator is given, the response is not finished when the
798
method returns. It is up to the request handler to call self.finish()
799
to finish the HTTP request. Without this decorator, the request is
800
automatically finished when the get() or post() method returns.
802
class MyRequestHandler(web.RequestHandler):
805
http = httpclient.AsyncHTTPClient()
806
http.fetch("http://friendfeed.com/", self._on_download)
808
def _on_download(self, response):
809
self.write("Downloaded!")
813
@functools.wraps(method)
814
def wrapper(self, *args, **kwargs):
815
if self.application._wsgi:
816
raise Exception("@asynchronous is not supported for WSGI apps")
817
self._auto_finish = False
818
return method(self, *args, **kwargs)
822
def removeslash(method):
823
"""Use this decorator to remove trailing slashes from the request path.
825
For example, a request to '/foo/' would redirect to '/foo' with this
826
decorator. Your request handler mapping should use a regular expression
827
like r'/foo/*' in conjunction with using the decorator.
829
@functools.wraps(method)
830
def wrapper(self, *args, **kwargs):
831
if self.request.path.endswith("/"):
832
if self.request.method == "GET":
833
uri = self.request.path.rstrip("/")
834
if self.request.query: uri += "?" + self.request.query
838
return method(self, *args, **kwargs)
842
def addslash(method):
843
"""Use this decorator to add a missing trailing slash to the request path.
845
For example, a request to '/foo' would redirect to '/foo/' with this
846
decorator. Your request handler mapping should use a regular expression
847
like r'/foo/?' in conjunction with using the decorator.
849
@functools.wraps(method)
850
def wrapper(self, *args, **kwargs):
851
if not self.request.path.endswith("/"):
852
if self.request.method == "GET":
853
uri = self.request.path + "/"
854
if self.request.query: uri += "?" + self.request.query
858
return method(self, *args, **kwargs)
862
class Application(object):
863
"""A collection of request handlers that make up a web application.
865
Instances of this class are callable and can be passed directly to
866
HTTPServer to serve the application:
868
application = web.Application([
869
(r"/", MainPageHandler),
871
http_server = httpserver.HTTPServer(application)
872
http_server.listen(8080)
873
ioloop.IOLoop.instance().start()
875
The constructor for this class takes in a list of URLSpec objects
876
or (regexp, request_class) tuples. When we receive requests, we
877
iterate over the list in order and instantiate an instance of the
878
first request class whose regexp matches the request path.
880
Each tuple can contain an optional third element, which should be a
881
dictionary if it is present. That dictionary is passed as keyword
882
arguments to the contructor of the handler. This pattern is used
883
for the StaticFileHandler below:
885
application = web.Application([
886
(r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
889
We support virtual hosts with the add_handlers method, which takes in
890
a host regular expression as the first argument:
892
application.add_handlers(r"www\.myhost\.com", [
893
(r"/article/([0-9]+)", ArticleHandler),
896
You can serve static files by sending the static_path setting as a
897
keyword argument. We will serve those files from the /static/ URI
898
(this is configurable with the static_url_prefix setting),
899
and we will serve /favicon.ico and /robots.txt from the same directory.
901
def __init__(self, handlers=None, default_host="", transforms=None,
902
wsgi=False, **settings):
903
if transforms is None:
905
if settings.get("gzip"):
906
self.transforms.append(GZipContentEncoding)
907
self.transforms.append(ChunkedTransferEncoding)
909
self.transforms = transforms
911
self.named_handlers = {}
912
self.default_host = default_host
913
self.settings = settings
917
self._load_ui_modules(settings.get("ui_modules", {}))
918
self._load_ui_methods(settings.get("ui_methods", {}))
919
if self.settings.get("static_path"):
920
path = self.settings["static_path"]
921
handlers = list(handlers or [])
922
static_url_prefix = settings.get("static_url_prefix",
925
(re.escape(static_url_prefix) + r"(.*)", StaticFileHandler,
927
(r"/(favicon\.ico)", StaticFileHandler, dict(path=path)),
928
(r"/(robots\.txt)", StaticFileHandler, dict(path=path)),
930
if handlers: self.add_handlers(".*$", handlers)
932
# Automatically reload modified modules
933
if self.settings.get("debug") and not wsgi:
937
def add_handlers(self, host_pattern, host_handlers):
938
"""Appends the given handlers to our handler list."""
939
if not host_pattern.endswith("$"):
942
# The handlers with the wildcard host_pattern are a special
943
# case - they're added in the constructor but should have lower
944
# precedence than the more-precise handlers added later.
945
# If a wildcard handler group exists, it should always be last
946
# in the list, so insert new groups just before it.
947
if self.handlers and self.handlers[-1][0].pattern == '.*$':
948
self.handlers.insert(-1, (re.compile(host_pattern), handlers))
950
self.handlers.append((re.compile(host_pattern), handlers))
952
for spec in host_handlers:
953
if type(spec) is type(()):
954
assert len(spec) in (2, 3)
961
spec = URLSpec(pattern, handler, kwargs)
962
handlers.append(spec)
964
if spec.name in self.named_handlers:
966
"Multiple handlers named %s; replacing previous value",
968
self.named_handlers[spec.name] = spec
970
def add_transform(self, transform_class):
971
"""Adds the given OutputTransform to our transform list."""
972
self.transforms.append(transform_class)
974
def _get_host_handlers(self, request):
975
host = request.host.lower().split(':')[0]
976
for pattern, handlers in self.handlers:
977
if pattern.match(host):
979
# Look for default host if not behind load balancer (for debugging)
980
if "X-Real-Ip" not in request.headers:
981
for pattern, handlers in self.handlers:
982
if pattern.match(self.default_host):
986
def _load_ui_methods(self, methods):
987
if type(methods) is types.ModuleType:
988
self._load_ui_methods(dict((n, getattr(methods, n))
989
for n in dir(methods)))
990
elif isinstance(methods, list):
991
for m in list: self._load_ui_methods(m)
993
for name, fn in methods.iteritems():
994
if not name.startswith("_") and hasattr(fn, "__call__") \
995
and name[0].lower() == name[0]:
996
self.ui_methods[name] = fn
998
def _load_ui_modules(self, modules):
999
if type(modules) is types.ModuleType:
1000
self._load_ui_modules(dict((n, getattr(modules, n))
1001
for n in dir(modules)))
1002
elif isinstance(modules, list):
1003
for m in list: self._load_ui_modules(m)
1005
assert isinstance(modules, dict)
1006
for name, cls in modules.iteritems():
1008
if issubclass(cls, UIModule):
1009
self.ui_modules[name] = cls
1013
def __call__(self, request):
1014
"""Called by HTTPServer to execute the request."""
1015
transforms = [t(request) for t in self.transforms]
1019
handlers = self._get_host_handlers(request)
1021
handler = RedirectHandler(
1022
request, "http://" + self.default_host + "/")
1024
for spec in handlers:
1025
match = spec.regex.match(request.path)
1027
handler = spec.handler_class(self, request, **spec.kwargs)
1028
# Pass matched groups to the handler. Since
1029
# match.groups() includes both named and unnamed groups,
1030
# we want to use either groups or groupdict but not both.
1031
kwargs = match.groupdict()
1035
args = match.groups()
1038
handler = ErrorHandler(self, request, 404)
1040
# In debug mode, re-compile templates and reload static files on every
1041
# request so you don't need to restart to see changes
1042
if self.settings.get("debug"):
1043
if getattr(RequestHandler, "_templates", None):
1044
map(lambda loader: loader.reset(),
1045
RequestHandler._templates.values())
1046
RequestHandler._static_hashes = {}
1048
handler._execute(transforms, *args, **kwargs)
1051
def reverse_url(self, name, *args):
1052
"""Returns a URL path for handler named `name`
1054
The handler must be added to the application as a named URLSpec
1056
if name in self.named_handlers:
1057
return self.named_handlers[name].reverse(*args)
1058
raise KeyError("%s not found in named urls" % name)
1061
class HTTPError(Exception):
1062
"""An exception that will turn into an HTTP error response."""
1063
def __init__(self, status_code, log_message=None, *args):
1064
self.status_code = status_code
1065
self.log_message = log_message
1069
message = "HTTP %d: %s" % (
1070
self.status_code, httplib.responses[self.status_code])
1071
if self.log_message:
1072
return message + " (" + (self.log_message % self.args) + ")"
1077
class ErrorHandler(RequestHandler):
1078
"""Generates an error response with status_code for all requests."""
1079
def __init__(self, application, request, status_code):
1080
RequestHandler.__init__(self, application, request)
1081
self.set_status(status_code)
1084
raise HTTPError(self._status_code)
1087
class RedirectHandler(RequestHandler):
1088
"""Redirects the client to the given URL for all GET requests.
1090
You should provide the keyword argument "url" to the handler, e.g.:
1092
application = web.Application([
1093
(r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
1096
def __init__(self, application, request, url, permanent=True):
1097
RequestHandler.__init__(self, application, request)
1099
self._permanent = permanent
1102
self.redirect(self._url, permanent=self._permanent)
1105
class StaticFileHandler(RequestHandler):
1106
"""A simple handler that can serve static content from a directory.
1108
To map a path to this handler for a static data directory /var/www,
1109
you would add a line to your application like:
1111
application = web.Application([
1112
(r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
1115
The local root directory of the content should be passed as the "path"
1116
argument to the handler.
1118
To support aggressive browser caching, if the argument "v" is given
1119
with the path, we set an infinite HTTP expiration header. So, if you
1120
want browsers to cache a file indefinitely, send them to, e.g.,
1121
/static/images/myimage.png?v=xxx.
1123
def __init__(self, application, request, path):
1124
RequestHandler.__init__(self, application, request)
1125
self.root = os.path.abspath(path) + os.path.sep
1127
def head(self, path):
1128
self.get(path, include_body=False)
1130
def get(self, path, include_body=True):
1131
abspath = os.path.abspath(os.path.join(self.root, path))
1132
if not abspath.startswith(self.root):
1133
raise HTTPError(403, "%s is not in root static directory", path)
1134
if not os.path.exists(abspath):
1135
raise HTTPError(404)
1136
if not os.path.isfile(abspath):
1137
raise HTTPError(403, "%s is not a file", path)
1139
stat_result = os.stat(abspath)
1140
modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
1142
self.set_header("Last-Modified", modified)
1143
if "v" in self.request.arguments:
1144
self.set_header("Expires", datetime.datetime.utcnow() + \
1145
datetime.timedelta(days=365*10))
1146
self.set_header("Cache-Control", "max-age=" + str(86400*365*10))
1148
self.set_header("Cache-Control", "public")
1149
mime_type, encoding = mimetypes.guess_type(abspath)
1151
self.set_header("Content-Type", mime_type)
1153
# Check the If-Modified-Since, and don't send the result if the
1154
# content has not been modified
1155
ims_value = self.request.headers.get("If-Modified-Since")
1156
if ims_value is not None:
1157
date_tuple = email.utils.parsedate(ims_value)
1158
if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
1159
if if_since >= modified:
1160
self.set_status(304)
1163
if not include_body:
1165
self.set_header("Content-Length", stat_result[stat.ST_SIZE])
1166
file = open(abspath, "rb")
1168
self.write(file.read())
1173
class FallbackHandler(RequestHandler):
1174
"""A RequestHandler that wraps another HTTP server callback.
1176
The fallback is a callable object that accepts an HTTPRequest,
1177
such as an Application or tornado.wsgi.WSGIContainer. This is most
1178
useful to use both tornado RequestHandlers and WSGI in the same server.
1180
wsgi_app = tornado.wsgi.WSGIContainer(
1181
django.core.handlers.wsgi.WSGIHandler())
1182
application = tornado.web.Application([
1183
(r"/foo", FooHandler),
1184
(r".*", FallbackHandler, dict(fallback=wsgi_app),
1187
def __init__(self, app, request, fallback):
1188
RequestHandler.__init__(self, app, request)
1189
self.fallback = fallback
1192
self.fallback(self.request)
1193
self._finished = True
1196
class OutputTransform(object):
1197
"""A transform modifies the result of an HTTP request (e.g., GZip encoding)
1199
A new transform instance is created for every request. See the
1200
ChunkedTransferEncoding example below if you want to implement a
1203
def __init__(self, request):
1206
def transform_first_chunk(self, headers, chunk, finishing):
1207
return headers, chunk
1209
def transform_chunk(self, chunk, finishing):
1213
class GZipContentEncoding(OutputTransform):
1214
"""Applies the gzip content encoding to the response.
1216
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
1218
CONTENT_TYPES = set([
1219
"text/plain", "text/html", "text/css", "text/xml",
1220
"application/x-javascript", "application/xml", "application/atom+xml",
1221
"text/javascript", "application/json", "application/xhtml+xml"])
1224
def __init__(self, request):
1225
self._gzipping = request.supports_http_1_1() and \
1226
"gzip" in request.headers.get("Accept-Encoding", "")
1228
def transform_first_chunk(self, headers, chunk, finishing):
1230
ctype = headers.get("Content-Type", "").split(";")[0]
1231
self._gzipping = (ctype in self.CONTENT_TYPES) and \
1232
(not finishing or len(chunk) >= self.MIN_LENGTH) and \
1233
(finishing or "Content-Length" not in headers) and \
1234
("Content-Encoding" not in headers)
1236
headers["Content-Encoding"] = "gzip"
1237
self._gzip_value = cStringIO.StringIO()
1238
self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
1240
chunk = self.transform_chunk(chunk, finishing)
1241
if "Content-Length" in headers:
1242
headers["Content-Length"] = str(len(chunk))
1243
return headers, chunk
1245
def transform_chunk(self, chunk, finishing):
1247
self._gzip_file.write(chunk)
1249
self._gzip_file.close()
1251
self._gzip_file.flush()
1252
chunk = self._gzip_value.getvalue()
1253
if self._gzip_pos > 0:
1254
chunk = chunk[self._gzip_pos:]
1255
self._gzip_pos += len(chunk)
1259
class ChunkedTransferEncoding(OutputTransform):
1260
"""Applies the chunked transfer encoding to the response.
1262
See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
1264
def __init__(self, request):
1265
self._chunking = request.supports_http_1_1()
1267
def transform_first_chunk(self, headers, chunk, finishing):
1269
# No need to chunk the output if a Content-Length is specified
1270
if "Content-Length" in headers or "Transfer-Encoding" in headers:
1271
self._chunking = False
1273
headers["Transfer-Encoding"] = "chunked"
1274
chunk = self.transform_chunk(chunk, finishing)
1275
return headers, chunk
1277
def transform_chunk(self, block, finishing):
1279
# Don't write out empty chunks because that means END-OF-STREAM
1280
# with chunked encoding
1282
block = ("%x" % len(block)) + "\r\n" + block + "\r\n"
1284
block += "0\r\n\r\n"
1288
def authenticated(method):
1289
"""Decorate methods with this to require that the user be logged in."""
1290
@functools.wraps(method)
1291
def wrapper(self, *args, **kwargs):
1292
if not self.current_user:
1293
if self.request.method == "GET":
1294
url = self.get_login_url()
1296
url += "?" + urllib.urlencode(dict(next=self.request.uri))
1299
raise HTTPError(403)
1300
return method(self, *args, **kwargs)
1304
class UIModule(object):
1305
"""A UI re-usable, modular unit on a page.
1307
UI modules often execute additional queries, and they can include
1308
additional CSS and JavaScript that will be included in the output
1309
page, which is automatically inserted on page render.
1311
def __init__(self, handler):
1312
self.handler = handler
1313
self.request = handler.request
1314
self.ui = handler.ui
1315
self.current_user = handler.current_user
1316
self.locale = handler.locale
1318
def render(self, *args, **kwargs):
1319
raise NotImplementedError()
1321
def embedded_javascript(self):
1322
"""Returns a JavaScript string that will be embedded in the page."""
1325
def javascript_files(self):
1326
"""Returns a list of JavaScript files required by this module."""
1329
def embedded_css(self):
1330
"""Returns a CSS string that will be embedded in the page."""
1333
def css_files(self):
1334
"""Returns a list of JavaScript files required by this module."""
1337
def html_head(self):
1338
"""Returns a CSS string that will be put in the <head/> element"""
1341
def html_body(self):
1342
"""Returns an HTML string that will be put in the <body/> element"""
1345
def render_string(self, path, **kwargs):
1346
return self.handler.render_string(path, **kwargs)
1348
class URLSpec(object):
1349
"""Specifies mappings between URLs and handlers."""
1350
def __init__(self, pattern, handler_class, kwargs={}, name=None):
1351
"""Creates a URLSpec.
1354
pattern: Regular expression to be matched. Any groups in the regex
1355
will be passed in to the handler's get/post/etc methods as
1357
handler_class: RequestHandler subclass to be invoked.
1358
kwargs (optional): A dictionary of additional arguments to be passed
1359
to the handler's constructor.
1360
name (optional): A name for this handler. Used by
1361
Application.reverse_url.
1363
if not pattern.endswith('$'):
1365
self.regex = re.compile(pattern)
1366
self.handler_class = handler_class
1367
self.kwargs = kwargs
1369
self._path, self._group_count = self._find_groups()
1371
def _find_groups(self):
1372
"""Returns a tuple (reverse string, group count) for a url.
1374
For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
1375
would return ('/%s/%s/', 2).
1377
pattern = self.regex.pattern
1378
if pattern.startswith('^'):
1379
pattern = pattern[1:]
1380
if pattern.endswith('$'):
1381
pattern = pattern[:-1]
1383
if self.regex.groups != pattern.count('('):
1384
# The pattern is too complicated for our simplistic matching,
1385
# so we can't support reversing it.
1389
for fragment in pattern.split('('):
1391
paren_loc = fragment.index(')')
1393
pieces.append('%s' + fragment[paren_loc + 1:])
1395
pieces.append(fragment)
1397
return (''.join(pieces), self.regex.groups)
1399
def reverse(self, *args):
1400
assert self._path is not None, \
1401
"Cannot reverse url regex " + self.regex.pattern
1402
assert len(args) == self._group_count, "required number of arguments "\
1406
return self._path % tuple([str(a) for a in args])
1411
if isinstance(s, unicode):
1412
return s.encode("utf-8")
1413
assert isinstance(s, str)
1418
if isinstance(s, str):
1420
return s.decode("utf-8")
1421
except UnicodeDecodeError:
1422
raise HTTPError(400, "Non-utf8 argument")
1423
assert isinstance(s, unicode)
1427
def _time_independent_equals(a, b):
1428
if len(a) != len(b):
1431
for x, y in zip(a, b):
1432
result |= ord(x) ^ ord(y)
1437
"""Makes a dictionary behave like an object."""
1438
def __getattr__(self, name):
1442
raise AttributeError(name)
1444
def __setattr__(self, name, value):