Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGonzalo Odiard <godiard@gmail.com>2013-04-25 13:15:05 (GMT)
committer Gonzalo Odiard <godiard@gmail.com>2013-04-25 13:15:05 (GMT)
commitbcb977f7a764d34a34454fed0b03f6e890bbc4eb (patch)
treebfc9d0fede23e99f80d037eee2aa5f2ed093036e
parent1cdce0801565b5d3d74a5434df993153946e5d57 (diff)
Use tornado as web server
In preparation to use websockets to notify the clients of changes, changed the server to use tornado instead of BasicHttpServer. The code is simplified and works fast. I added the needed tornado files to the activity until we decide if will be used as a global solution for web activities. Signed-off-by: Gonzalo Odiard <gonzalo@laptop.org>
-rw-r--r--server.py201
-rw-r--r--tornado/__init__.py26
-rw-r--r--tornado/autoreload.py249
-rw-r--r--tornado/escape.py326
-rw-r--r--tornado/httpserver.py475
-rw-r--r--tornado/httputil.py279
-rw-r--r--tornado/ioloop.py642
-rw-r--r--tornado/iostream.py727
-rw-r--r--tornado/locale.py471
-rw-r--r--tornado/netutil.py319
-rw-r--r--tornado/platform/__init__.py1
-rw-r--r--tornado/platform/auto.py30
-rw-r--r--tornado/platform/interface.py56
-rw-r--r--tornado/platform/posix.py61
-rw-r--r--tornado/process.py148
-rw-r--r--tornado/stack_context.py243
-rw-r--r--tornado/template.py825
-rw-r--r--tornado/util.py47
-rw-r--r--tornado/web.py1984
-rw-r--r--tornado/websocket.py650
-rw-r--r--web/index.html2
21 files changed, 7620 insertions, 142 deletions
diff --git a/server.py b/server.py
index 459c8ca..561d75d 100644
--- a/server.py
+++ b/server.py
@@ -16,156 +16,75 @@
import os
import logging
-import cgi
-import BaseHTTPServer
-from SimpleHTTPServer import SimpleHTTPRequestHandler
-import SocketServer
-import select
+from tornado import httpserver
+from tornado import ioloop
+from tornado import web
from gi.repository import GLib
import utils
-class JournalHTTPRequestHandler(SimpleHTTPRequestHandler):
- """HTTP Request Handler to send data to the webview.
-
- RequestHandler class that integrates with Glib mainloop. It writes
- the specified file to the client in chunks, returning control to the
- mainloop between chunks.
-
- """
-
- def __init__(self, activity_path, activity_root, jm, request,
- client_address, server):
- self.activity_path = activity_path
- self.activity_root = activity_root
- self.jm = jm
- SimpleHTTPRequestHandler.__init__(self, request, client_address,
- server)
-
- def do_POST(self):
- if self.path == '/datastore/upload':
- ctype = self.headers.get('content-type')
- if not ctype:
- return None
- ctype, pdict = cgi.parse_header(ctype)
- query = cgi.parse_multipart(self.rfile, pdict)
-
- file_content = query.get('journal_item')[0]
- # save to the journal
- zipped_file_path = os.path.join(self.activity_root,
- 'instance', 'received.journal')
- f = open(zipped_file_path, 'wb')
- try:
- f.write(file_content)
- finally:
- f.close()
-
- metadata, preview_data, file_path = \
- utils.unpackage_ds_object(zipped_file_path, None)
-
- logging.error('METADATA %s', metadata)
-
- GLib.idle_add(self.jm.create_object, file_path, metadata,
- preview_data)
-
- #redirect to index.html page
- self.send_header_response("text/html")
-
- self.send_file(os.path.join(self.activity_path,
- 'web/reload_index.html'))
-
- def do_GET(self):
- """Respond to a GET request."""
- #logging.error('inside do_get dir(self) %s', dir(self))
-
- if self.path:
- logging.error('Requested path %s', self.path)
- if self.path.startswith('/web'):
- # TODO: check mime_type
- self.send_header_response("text/html")
- # return files requested in the web directory
- file_path = self.activity_path + self.path
-
- if os.path.isfile(file_path):
- self.send_file(file_path)
-
- if self.path.startswith('/datastore'):
- # return files requested in the activity instance directory
- path = self.path.replace('datastore', 'instance')
- file_path = self.activity_root + path
-
- mime_type = 'text/html'
- if file_path.endswith('.journal'):
- mime_type = 'application/journal'
- self.send_header_response(mime_type)
-
- if os.path.isfile(file_path):
- self.send_file(file_path)
-
- def send_file(self, file_path):
- logging.error('Opening requested file %s', file_path)
- f = open(file_path)
- content = f.read()
+class UploaderHandler(web.RequestHandler):
+
+ def initialize(self, instance_path, static_path, journal_manager):
+ self.instance_path = instance_path
+ self.static_path = static_path
+ self.jm = journal_manager
+
+ def post(self):
+ journal_item = self.request.files['journal_item'][0]
+
+ # save to the journal
+ zipped_file_path = os.path.join(self.instance_path, 'received.journal')
+ f = open(zipped_file_path, 'wb')
+ try:
+ f.write(journal_item['body'])
+ finally:
+ f.close()
+
+ metadata, preview_data, file_path = \
+ utils.unpackage_ds_object(zipped_file_path, None)
+
+ logging.error('METADATA %s', metadata)
+
+ GLib.idle_add(self.jm.create_object, file_path, metadata,
+ preview_data)
+
+ #redirect to index.html page
+ f = open(os.path.join(self.static_path, 'reload_index.html'))
+ self.write(f.read())
f.close()
- self.wfile.write(content)
-
- def send_header_response(self, mime_type, file_name=None):
- self.send_response(200)
- self.send_header("Content-type", mime_type)
- if file_name is not None:
- self.send_header("Content-Disposition",
- "inline; filename='%s'" % file_name)
- self.end_headers()
-
-
-class JournalHTTPServer(BaseHTTPServer.HTTPServer):
- """HTTP Server for transferring document while collaborating."""
-
- # from wikipedia activity
- def serve_forever(self, poll_interval=0.5):
- """Overridden version of BaseServer.serve_forever that does not fail
- to work when EINTR is received.
- """
- self._BaseServer__serving = True
- self._BaseServer__is_shut_down.clear()
- while self._BaseServer__serving:
-
- # XXX: Consider using another file descriptor or
- # connecting to the socket to wake this up instead of
- # polling. Polling reduces our responsiveness to a
- # shutdown request and wastes cpu at all other times.
- try:
- r, w, e = select.select([self], [], [], poll_interval)
- except select.error, e:
- if e[0] == errno.EINTR:
- logging.debug("got eintr")
- continue
- raise
- if r:
- self._handle_request_noblock()
- self._BaseServer__is_shut_down.set()
-
- def server_bind(self):
- """Override server_bind in HTTPServer to not use
- getfqdn to get the server name because is very slow."""
- SocketServer.TCPServer.server_bind(self)
- host, port = self.socket.getsockname()[:2]
- self.server_name = 'localhost'
- self.server_port = port
+ self.flush()
+
+
+class DatastoreHandler(web.StaticFileHandler):
+
+ def set_extra_headers(self, path):
+ """For subclass to add extra headers to the response"""
+ self.set_header("Content-Type", 'application/journal')
def run_server(activity_path, activity_root, jm, port):
- # init the journal manager before start the thread
+
from threading import Thread
- httpd = JournalHTTPServer(
- ("", port),
- lambda *args: JournalHTTPRequestHandler(activity_path, activity_root,
- jm, *args))
- server = Thread(target=httpd.serve_forever)
- server.setDaemon(True)
- logging.debug("Before start server")
- server.start()
- logging.debug("After start server")
+ io_loop = ioloop.IOLoop.instance()
+
+ static_path = os.path.join(activity_path, 'web')
+ instance_path = os.path.join(activity_root, 'instance')
+
+ application = web.Application(
+ [
+ (r"/web/(.*)", web.StaticFileHandler, {"path": static_path}),
+ (r"/datastore/(.*)", DatastoreHandler, {"path": instance_path}),
+ (r"/upload", UploaderHandler, {"instance_path": instance_path,
+ "static_path": static_path,
+ "journal_manager": jm
+ })
+ ])
+ http_server = httpserver.HTTPServer(application)
+ http_server.listen(port)
+ tornado_looop = Thread(target=io_loop.start)
+ tornado_looop.setDaemon(True)
+ tornado_looop.start()
diff --git a/tornado/__init__.py b/tornado/__init__.py
new file mode 100644
index 0000000..8194f49
--- /dev/null
+++ b/tornado/__init__.py
@@ -0,0 +1,26 @@
+#
+# 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.
+
+"""The Tornado web server and tools."""
+
+# version is a human-readable version number.
+
+# version_info is a four-tuple for programmatic comparison. The first
+# three numbers are the components of the version number. The fourth
+# is zero for an official release, positive for a development branch,
+# or negative for a release candidate (after the base version number
+# has been incremented)
+version = "2.2.1"
+version_info = (2, 2, 1, 0)
diff --git a/tornado/autoreload.py b/tornado/autoreload.py
new file mode 100644
index 0000000..4a7b906
--- /dev/null
+++ b/tornado/autoreload.py
@@ -0,0 +1,249 @@
+#
+# 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.
+
+"""A module to automatically restart the server when a module is modified.
+
+Most applications should not call this module directly. Instead, pass the
+keyword argument ``debug=True`` to the `tornado.web.Application` constructor.
+This will enable autoreload mode as well as checking for changes to templates
+and static resources.
+
+This module depends on IOLoop, so it will not work in WSGI applications
+and Google AppEngine. It also will not work correctly when HTTPServer's
+multi-process mode is used.
+"""
+
+from __future__ import with_statement
+
+import functools
+import logging
+import os
+import pkgutil
+import sys
+import types
+import subprocess
+
+from tornado import ioloop
+from tornado import process
+
+try:
+ import signal
+except ImportError:
+ signal = None
+
+def start(io_loop=None, check_time=500):
+ """Restarts the process automatically when a module is modified.
+
+ We run on the I/O loop, and restarting is a destructive operation,
+ so will terminate any pending requests.
+ """
+ io_loop = io_loop or ioloop.IOLoop.instance()
+ add_reload_hook(functools.partial(_close_all_fds, io_loop))
+ modify_times = {}
+ callback = functools.partial(_reload_on_update, modify_times)
+ scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
+ scheduler.start()
+
+def wait():
+ """Wait for a watched file to change, then restart the process.
+
+ Intended to be used at the end of scripts like unit test runners,
+ to run the tests again after any source file changes (but see also
+ the command-line interface in `main`)
+ """
+ io_loop = ioloop.IOLoop()
+ start(io_loop)
+ io_loop.start()
+
+_watched_files = set()
+
+def watch(filename):
+ """Add a file to the watch list.
+
+ All imported modules are watched by default.
+ """
+ _watched_files.add(filename)
+
+_reload_hooks = []
+
+def add_reload_hook(fn):
+ """Add a function to be called before reloading the process.
+
+ Note that for open file and socket handles it is generally
+ preferable to set the ``FD_CLOEXEC`` flag (using `fcntl` or
+ `tornado.platform.auto.set_close_exec`) instead of using a reload
+ hook to close them.
+ """
+ _reload_hooks.append(fn)
+
+def _close_all_fds(io_loop):
+ for fd in io_loop._handlers.keys():
+ try:
+ os.close(fd)
+ except Exception:
+ pass
+
+_reload_attempted = False
+
+def _reload_on_update(modify_times):
+ if _reload_attempted:
+ # We already tried to reload and it didn't work, so don't try again.
+ return
+ if process.task_id() is not None:
+ # We're in a child process created by fork_processes. If child
+ # processes restarted themselves, they'd all restart and then
+ # all call fork_processes again.
+ return
+ for module in sys.modules.values():
+ # Some modules play games with sys.modules (e.g. email/__init__.py
+ # in the standard library), and occasionally this can cause strange
+ # failures in getattr. Just ignore anything that's not an ordinary
+ # module.
+ if not isinstance(module, types.ModuleType): continue
+ path = getattr(module, "__file__", None)
+ if not path: continue
+ if path.endswith(".pyc") or path.endswith(".pyo"):
+ path = path[:-1]
+ _check_file(modify_times, path)
+ for path in _watched_files:
+ _check_file(modify_times, path)
+
+def _check_file(modify_times, path):
+ try:
+ modified = os.stat(path).st_mtime
+ except Exception:
+ return
+ if path not in modify_times:
+ modify_times[path] = modified
+ return
+ if modify_times[path] != modified:
+ logging.info("%s modified; restarting server", path)
+ _reload()
+
+def _reload():
+ global _reload_attempted
+ _reload_attempted = True
+ for fn in _reload_hooks:
+ fn()
+ if hasattr(signal, "setitimer"):
+ # Clear the alarm signal set by
+ # ioloop.set_blocking_log_threshold so it doesn't fire
+ # after the exec.
+ signal.setitimer(signal.ITIMER_REAL, 0, 0)
+ if sys.platform == 'win32':
+ # os.execv is broken on Windows and can't properly parse command line
+ # arguments and executable name if they contain whitespaces. subprocess
+ # fixes that behavior.
+ subprocess.Popen([sys.executable] + sys.argv)
+ sys.exit(0)
+ else:
+ try:
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+ except OSError:
+ # Mac OS X versions prior to 10.6 do not support execv in
+ # a process that contains multiple threads. Instead of
+ # re-executing in the current process, start a new one
+ # and cause the current process to exit. This isn't
+ # ideal since the new process is detached from the parent
+ # terminal and thus cannot easily be killed with ctrl-C,
+ # but it's better than not being able to autoreload at
+ # all.
+ # Unfortunately the errno returned in this case does not
+ # appear to be consistent, so we can't easily check for
+ # this error specifically.
+ os.spawnv(os.P_NOWAIT, sys.executable,
+ [sys.executable] + sys.argv)
+ sys.exit(0)
+
+_USAGE = """\
+Usage:
+ python -m tornado.autoreload -m module.to.run [args...]
+ python -m tornado.autoreload path/to/script.py [args...]
+"""
+def main():
+ """Command-line wrapper to re-run a script whenever its source changes.
+
+ Scripts may be specified by filename or module name::
+
+ python -m tornado.autoreload -m tornado.test.runtests
+ python -m tornado.autoreload tornado/test/runtests.py
+
+ Running a script with this wrapper is similar to calling
+ `tornado.autoreload.wait` at the end of the script, but this wrapper
+ can catch import-time problems like syntax errors that would otherwise
+ prevent the script from reaching its call to `wait`.
+ """
+ original_argv = sys.argv
+ sys.argv = sys.argv[:]
+ if len(sys.argv) >= 3 and sys.argv[1] == "-m":
+ mode = "module"
+ module = sys.argv[2]
+ del sys.argv[1:3]
+ elif len(sys.argv) >= 2:
+ mode = "script"
+ script = sys.argv[1]
+ sys.argv = sys.argv[1:]
+ else:
+ print >>sys.stderr, _USAGE
+ sys.exit(1)
+
+ try:
+ if mode == "module":
+ import runpy
+ runpy.run_module(module, run_name="__main__", alter_sys=True)
+ elif mode == "script":
+ with open(script) as f:
+ global __file__
+ __file__ = script
+ # Use globals as our "locals" dictionary so that
+ # something that tries to import __main__ (e.g. the unittest
+ # module) will see the right things.
+ exec f.read() in globals(), globals()
+ except SystemExit, e:
+ logging.info("Script exited with status %s", e.code)
+ except Exception, e:
+ logging.warning("Script exited with uncaught exception", exc_info=True)
+ if isinstance(e, SyntaxError):
+ watch(e.filename)
+ else:
+ logging.info("Script exited normally")
+ # restore sys.argv so subsequent executions will include autoreload
+ sys.argv = original_argv
+
+ if mode == 'module':
+ # runpy did a fake import of the module as __main__, but now it's
+ # no longer in sys.modules. Figure out where it is and watch it.
+ watch(pkgutil.get_loader(module).get_filename())
+
+ wait()
+
+
+if __name__ == "__main__":
+ # If this module is run with "python -m tornado.autoreload", the current
+ # directory is automatically prepended to sys.path, but not if it is
+ # run as "path/to/tornado/autoreload.py". The processing for "-m" rewrites
+ # the former to the latter, so subsequent executions won't have the same
+ # path as the original. Modify os.environ here to ensure that the
+ # re-executed process will have the same path.
+ # Conversely, when run as path/to/tornado/autoreload.py, the directory
+ # containing autoreload.py gets added to the path, but we don't want
+ # tornado modules importable at top level, so remove it.
+ path_prefix = '.' + os.pathsep
+ if (sys.path[0] == '' and
+ not os.environ.get("PYTHONPATH", "").startswith(path_prefix)):
+ os.environ["PYTHONPATH"] = path_prefix + os.environ.get("PYTHONPATH", "")
+ elif sys.path[0] == os.path.dirname(__file__):
+ del sys.path[0]
+ main()
diff --git a/tornado/escape.py b/tornado/escape.py
new file mode 100644
index 0000000..6a23352
--- /dev/null
+++ b/tornado/escape.py
@@ -0,0 +1,326 @@
+#
+# 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.
+
+"""Escaping/unescaping methods for HTML, JSON, URLs, and others.
+
+Also includes a few other miscellaneous string manipulation functions that
+have crept in over time.
+"""
+
+import htmlentitydefs
+import re
+import sys
+import urllib
+
+# Python3 compatibility: On python2.5, introduce the bytes alias from 2.6
+try: bytes
+except Exception: bytes = str
+
+try:
+ from urlparse import parse_qs # Python 2.6+
+except ImportError:
+ from cgi import parse_qs
+
+# json module is in the standard library as of python 2.6; fall back to
+# simplejson if present for older versions.
+try:
+ import json
+ assert hasattr(json, "loads") and hasattr(json, "dumps")
+ _json_decode = json.loads
+ _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
+
+
+_XHTML_ESCAPE_RE = re.compile('[&<>"]')
+_XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}
+def xhtml_escape(value):
+ """Escapes a string so it is valid within XML or XHTML."""
+ return _XHTML_ESCAPE_RE.sub(lambda match: _XHTML_ESCAPE_DICT[match.group(0)],
+ to_basestring(value))
+
+
+def xhtml_unescape(value):
+ """Un-escapes an XML-escaped string."""
+ return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
+
+
+def json_encode(value):
+ """JSON-encodes the given Python object."""
+ # JSON permits but does not require forward slashes to be escaped.
+ # This is useful when json data is emitted in a <script> tag
+ # in HTML, as it prevents </script> tags from prematurely terminating
+ # the javscript. Some json libraries do this escaping by default,
+ # although python's standard library does not, so we do it here.
+ # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped
+ return _json_encode(recursive_unicode(value)).replace("</", "<\\/")
+
+
+def json_decode(value):
+ """Returns Python objects for the given JSON string."""
+ return _json_decode(to_basestring(value))
+
+
+def squeeze(value):
+ """Replace all sequences of whitespace chars with a single space."""
+ return re.sub(r"[\x00-\x20]+", " ", value).strip()
+
+
+def url_escape(value):
+ """Returns a valid URL-encoded version of the given value."""
+ return urllib.quote_plus(utf8(value))
+
+# python 3 changed things around enough that we need two separate
+# implementations of url_unescape. We also need our own implementation
+# of parse_qs since python 3's version insists on decoding everything.
+if sys.version_info[0] < 3:
+ def url_unescape(value, encoding='utf-8'):
+ """Decodes the given value from a URL.
+
+ The argument may be either a byte or unicode string.
+
+ If encoding is None, the result will be a byte string. Otherwise,
+ the result is a unicode string in the specified encoding.
+ """
+ if encoding is None:
+ return urllib.unquote_plus(utf8(value))
+ else:
+ return unicode(urllib.unquote_plus(utf8(value)), encoding)
+
+ parse_qs_bytes = parse_qs
+else:
+ def url_unescape(value, encoding='utf-8'):
+ """Decodes the given value from a URL.
+
+ The argument may be either a byte or unicode string.
+
+ If encoding is None, the result will be a byte string. Otherwise,
+ the result is a unicode string in the specified encoding.
+ """
+ if encoding is None:
+ return urllib.parse.unquote_to_bytes(value)
+ else:
+ return urllib.unquote_plus(to_basestring(value), encoding=encoding)
+
+ def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False):
+ """Parses a query string like urlparse.parse_qs, but returns the
+ values as byte strings.
+
+ Keys still become type str (interpreted as latin1 in python3!)
+ because it's too painful to keep them as byte strings in
+ python3 and in practice they're nearly always ascii anyway.
+ """
+ # This is gross, but python3 doesn't give us another way.
+ # Latin1 is the universal donor of character encodings.
+ result = parse_qs(qs, keep_blank_values, strict_parsing,
+ encoding='latin1', errors='strict')
+ encoded = {}
+ for k,v in result.iteritems():
+ encoded[k] = [i.encode('latin1') for i in v]
+ return encoded
+
+
+
+_UTF8_TYPES = (bytes, type(None))
+def utf8(value):
+ """Converts a string argument to a byte string.
+
+ If the argument is already a byte string or None, it is returned unchanged.
+ Otherwise it must be a unicode string and is encoded as utf8.
+ """
+ if isinstance(value, _UTF8_TYPES):
+ return value
+ assert isinstance(value, unicode)
+ return value.encode("utf-8")
+
+_TO_UNICODE_TYPES = (unicode, type(None))
+def to_unicode(value):
+ """Converts a string argument to a unicode string.
+
+ If the argument is already a unicode string or None, it is returned
+ unchanged. Otherwise it must be a byte string and is decoded as utf8.
+ """
+ if isinstance(value, _TO_UNICODE_TYPES):
+ return value
+ assert isinstance(value, bytes)
+ return value.decode("utf-8")
+
+# to_unicode was previously named _unicode not because it was private,
+# but to avoid conflicts with the built-in unicode() function/type
+_unicode = to_unicode
+
+# 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
+if str is unicode:
+ native_str = to_unicode
+else:
+ native_str = utf8
+
+_BASESTRING_TYPES = (basestring, type(None))
+def to_basestring(value):
+ """Converts a string argument to a subclass of basestring.
+
+ In python2, byte and unicode strings are mostly interchangeable,
+ so functions that deal with a user-supplied argument in combination
+ with ascii string constants can use either and should return the type
+ the user supplied. In python3, the two types are not interchangeable,
+ so this method is needed to convert byte strings to unicode.
+ """
+ if isinstance(value, _BASESTRING_TYPES):
+ return value
+ assert isinstance(value, bytes)
+ return value.decode("utf-8")
+
+def recursive_unicode(obj):
+ """Walks a simple data structure, converting byte strings to unicode.
+
+ Supports lists, tuples, and dictionaries.
+ """
+ if isinstance(obj, dict):
+ return dict((recursive_unicode(k), recursive_unicode(v)) for (k,v) in obj.iteritems())
+ elif isinstance(obj, list):
+ return list(recursive_unicode(i) for i in obj)
+ elif isinstance(obj, tuple):
+ return tuple(recursive_unicode(i) for i in obj)
+ elif isinstance(obj, bytes):
+ return to_unicode(obj)
+ else:
+ return obj
+
+# I originally used the regex from
+# http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+# but it gets all exponential on certain patterns (such as too many trailing
+# dots), causing the regex matcher to never return.
+# This regex should avoid those problems.
+_URL_RE = re.compile(ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)""")
+
+
+def linkify(text, shorten=False, extra_params="",
+ require_protocol=False, permitted_protocols=["http", "https"]):
+ """Converts plain text into HTML with links.
+
+ For example: ``linkify("Hello http://tornadoweb.org!")`` would return
+ ``Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!``
+
+ Parameters:
+
+ shorten: Long urls will be shortened for display.
+
+ extra_params: Extra text to include in the link tag,
+ e.g. linkify(text, extra_params='rel="nofollow" class="external"')
+
+ require_protocol: Only linkify urls which include a protocol. If this is
+ False, urls such as www.facebook.com will also be linkified.
+
+ permitted_protocols: List (or set) of protocols which should be linkified,
+ e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]).
+ It is very unsafe to include protocols such as "javascript".
+ """
+ if extra_params:
+ extra_params = " " + extra_params.strip()
+
+ def make_link(m):
+ url = m.group(1)
+ proto = m.group(2)
+ if require_protocol and not proto:
+ return url # not protocol, no linkify
+
+ if proto and proto not in permitted_protocols:
+ return url # bad protocol, no linkify
+
+ href = m.group(1)
+ if not proto:
+ href = "http://" + href # no proto specified, use http
+
+ params = extra_params
+
+ # clip long urls. max_len is just an approximation
+ max_len = 30
+ if shorten and len(url) > max_len:
+ before_clip = url
+ if proto:
+ proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for :
+ else:
+ proto_len = 0
+
+ parts = url[proto_len:].split("/")
+ if len(parts) > 1:
+ # Grab the whole host part plus the first bit of the path
+ # The path is usually not that interesting once shortened
+ # (no more slug, etc), so it really just provides a little
+ # extra indication of shortening.
+ url = url[:proto_len] + parts[0] + "/" + \
+ parts[1][:8].split('?')[0].split('.')[0]
+
+ if len(url) > max_len * 1.5: # still too long
+ url = url[:max_len]
+
+ if url != before_clip:
+ amp = url.rfind('&')
+ # avoid splitting html char entities
+ if amp > max_len - 5:
+ url = url[:amp]
+ url += "..."
+
+ if len(url) >= len(before_clip):
+ url = before_clip
+ else:
+ # full url is visible on mouse-over (for those who don't
+ # have a status bar, such as Safari by default)
+ params += ' title="%s"' % href
+
+ return u'<a href="%s"%s>%s</a>' % (href, params, url)
+
+ # First HTML-escape so that our strings are all safe.
+ # The regex is modified to avoid character entites other than &amp; so
+ # that we won't pick up &quot;, etc.
+ text = _unicode(xhtml_escape(text))
+ return _URL_RE.sub(make_link, text)
+
+
+def _convert_entity(m):
+ if m.group(1) == "#":
+ try:
+ return unichr(int(m.group(2)))
+ except ValueError:
+ return "&#%s;" % m.group(2)
+ try:
+ return _HTML_UNICODE_MAP[m.group(2)]
+ except KeyError:
+ return "&%s;" % m.group(2)
+
+
+def _build_unicode_map():
+ unicode_map = {}
+ for name, value in htmlentitydefs.name2codepoint.iteritems():
+ unicode_map[name] = unichr(value)
+ return unicode_map
+
+_HTML_UNICODE_MAP = _build_unicode_map()
diff --git a/tornado/httpserver.py b/tornado/httpserver.py
new file mode 100644
index 0000000..762902b
--- /dev/null
+++ b/tornado/httpserver.py
@@ -0,0 +1,475 @@
+#
+# 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.
+
+"""A non-blocking, single-threaded HTTP server.
+
+Typical applications have little direct interaction with the `HTTPServer`
+class except to start a server at the beginning of the process
+(and even that is often done indirectly via `tornado.web.Application.listen`).
+
+This module also defines the `HTTPRequest` class which is exposed via
+`tornado.web.RequestHandler.request`.
+"""
+
+import Cookie
+import logging
+import socket
+import time
+import urlparse
+
+from tornado.escape import utf8, native_str, parse_qs_bytes
+from tornado import httputil
+from tornado import iostream
+from tornado.netutil import TCPServer
+from tornado import stack_context
+from tornado.util import b, bytes_type
+
+try:
+ import ssl # Python 2.6+
+except ImportError:
+ ssl = None
+
+class HTTPServer(TCPServer):
+ r"""A non-blocking, single-threaded HTTP server.
+
+ A server is defined by a request callback that takes an HTTPRequest
+ instance as an argument and writes a valid HTTP response with
+ `HTTPRequest.write`. `HTTPRequest.finish` finishes the request (but does
+ not necessarily close the connection in the case of HTTP/1.1 keep-alive
+ requests). A simple example server that echoes back the URI you
+ requested::
+
+ import httpserver
+ import ioloop
+
+ def handle_request(request):
+ message = "You requested %s\n" % request.uri
+ request.write("HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n%s" % (
+ len(message), message))
+ request.finish()
+
+ http_server = httpserver.HTTPServer(handle_request)
+ http_server.listen(8888)
+ ioloop.IOLoop.instance().start()
+
+ `HTTPServer` is a very basic connection handler. Beyond parsing the
+ HTTP request body and headers, the only HTTP semantics implemented
+ in `HTTPServer` is HTTP/1.1 keep-alive connections. We do not, however,
+ implement chunked encoding, so the request callback must provide a
+ ``Content-Length`` header or implement chunked encoding for HTTP/1.1
+ requests for the server to run correctly for HTTP/1.1 clients. If
+ the request handler is unable to do this, you can provide the
+ ``no_keep_alive`` argument to the `HTTPServer` constructor, which will
+ ensure the connection is closed on every request no matter what HTTP
+ version the client is using.
+
+ If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme``
+ headers, which override the remote IP and HTTP scheme for all requests.
+ These headers are useful when running Tornado behind a reverse proxy or
+ load balancer.
+
+ `HTTPServer` 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"::
+
+ HTTPServer(applicaton, ssl_options={
+ "certfile": os.path.join(data_dir, "mydomain.crt"),
+ "keyfile": os.path.join(data_dir, "mydomain.key"),
+ })
+
+ `HTTPServer` initialization follows one of three patterns (the
+ initialization methods are defined on `tornado.netutil.TCPServer`):
+
+ 1. `~tornado.netutil.TCPServer.listen`: simple single-process::
+
+ server = HTTPServer(app)
+ server.listen(8888)
+ IOLoop.instance().start()
+
+ In many cases, `tornado.web.Application.listen` can be used to avoid
+ the need to explicitly create the `HTTPServer`.
+
+ 2. `~tornado.netutil.TCPServer.bind`/`~tornado.netutil.TCPServer.start`:
+ simple multi-process::
+
+ server = HTTPServer(app)
+ 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 `HTTPServer` constructor. `start` will always start
+ the server on the default singleton `IOLoop`.
+
+ 3. `~tornado.netutil.TCPServer.add_sockets`: advanced multi-process::
+
+ sockets = tornado.netutil.bind_sockets(8888)
+ tornado.process.fork_processes(0)
+ server = HTTPServer(app)
+ 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
+ `tornado.netutil.bind_sockets`.
+
+ """
+ def __init__(self, request_callback, no_keep_alive=False, io_loop=None,
+ xheaders=False, ssl_options=None, **kwargs):
+ self.request_callback = request_callback
+ self.no_keep_alive = no_keep_alive
+ self.xheaders = xheaders
+ TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
+ **kwargs)
+
+ def handle_stream(self, stream, address):
+ HTTPConnection(stream, address, self.request_callback,
+ self.no_keep_alive, self.xheaders)
+
+class _BadRequestException(Exception):
+ """Exception class for malformed HTTP requests."""
+ pass
+
+class HTTPConnection(object):
+ """Handles a connection to an HTTP client, executing HTTP requests.
+
+ We parse HTTP headers and bodies, and execute the request callback
+ until the HTTP conection is closed.
+ """
+ def __init__(self, stream, address, request_callback, no_keep_alive=False,
+ xheaders=False):
+ self.stream = stream
+ if self.stream.socket.family not in (socket.AF_INET, socket.AF_INET6):
+ # Unix (or other) socket; fake the remote address
+ address = ('0.0.0.0', 0)
+ self.address = address
+ self.request_callback = request_callback
+ self.no_keep_alive = no_keep_alive
+ self.xheaders = xheaders
+ self._request = None
+ self._request_finished = False
+ # Save stack context here, outside of any request. This keeps
+ # contexts from one request from leaking into the next.
+ self._header_callback = stack_context.wrap(self._on_headers)
+ self.stream.read_until(b("\r\n\r\n"), self._header_callback)
+ self._write_callback = None
+
+ def write(self, chunk, callback=None):
+ """Writes a chunk of output to the stream."""
+ assert self._request, "Request closed"
+ if not self.stream.closed():
+ self._write_callback = stack_context.wrap(callback)
+ self.stream.write(chunk, self._on_write_complete)
+
+ def finish(self):
+ """Finishes the request."""
+ assert self._request, "Request closed"
+ self._request_finished = True
+ if not self.stream.writing():
+ self._finish_request()
+
+ def _on_write_complete(self):
+ if self._write_callback is not None:
+ callback = self._write_callback
+ self._write_callback = None
+ callback()
+ # _on_write_complete is enqueued on the IOLoop whenever the
+ # IOStream's write buffer becomes empty, but it's possible for
+ # another callback that runs on the IOLoop before it to
+ # simultaneously write more data and finish the request. If
+ # there is still data in the IOStream, a future
+ # _on_write_complete will be responsible for calling
+ # _finish_request.
+ if self._request_finished and not self.stream.writing():
+ self._finish_request()
+
+ def _finish_request(self):
+ if self.no_keep_alive:
+ disconnect = True
+ else:
+ connection_header = self._request.headers.get("Connection")
+ if connection_header is not None:
+ connection_header = connection_header.lower()
+ if self._request.supports_http_1_1():
+ disconnect = connection_header == "close"
+ elif ("Content-Length" in self._request.headers
+ or self._request.method in ("HEAD", "GET")):
+ disconnect = connection_header != "keep-alive"
+ else:
+ disconnect = True
+ self._request = None
+ self._request_finished = False
+ if disconnect:
+ self.stream.close()
+ return
+ self.stream.read_until(b("\r\n\r\n"), self._header_callback)
+
+ def _on_headers(self, data):
+ try:
+ data = native_str(data.decode('latin1'))
+ eol = data.find("\r\n")
+ start_line = data[:eol]
+ try:
+ method, uri, version = start_line.split(" ")
+ except ValueError:
+ raise _BadRequestException("Malformed HTTP request line")
+ if not version.startswith("HTTP/"):
+ raise _BadRequestException("Malformed HTTP version in HTTP Request-Line")
+ headers = httputil.HTTPHeaders.parse(data[eol:])
+ self._request = HTTPRequest(
+ connection=self, method=method, uri=uri, version=version,
+ headers=headers, remote_ip=self.address[0])
+
+ content_length = headers.get("Content-Length")
+ if content_length:
+ content_length = int(content_length)
+ if content_length > self.stream.max_buffer_size:
+ raise _BadRequestException("Content-Length too long")
+ if headers.get("Expect") == "100-continue":
+ self.stream.write(b("HTTP/1.1 100 (Continue)\r\n\r\n"))
+ self.stream.read_bytes(content_length, self._on_request_body)
+ return
+
+ self.request_callback(self._request)
+ except _BadRequestException, e:
+ logging.info("Malformed HTTP request from %s: %s",
+ self.address[0], e)
+ self.stream.close()
+ return
+
+ def _on_request_body(self, data):
+ self._request.body = data
+ content_type = self._request.headers.get("Content-Type", "")
+ if self._request.method in ("POST", "PUT"):
+ if content_type.startswith("application/x-www-form-urlencoded"):
+ arguments = parse_qs_bytes(native_str(self._request.body))
+ for name, values in arguments.iteritems():
+ values = [v for v in values if v]
+ if values:
+ self._request.arguments.setdefault(name, []).extend(
+ values)
+ elif content_type.startswith("multipart/form-data"):
+ fields = content_type.split(";")
+ for field in fields:
+ k, sep, v = field.strip().partition("=")
+ if k == "boundary" and v:
+ httputil.parse_multipart_form_data(
+ utf8(v), data,
+ self._request.arguments,
+ self._request.files)
+ break
+ else:
+ logging.warning("Invalid multipart/form-data")
+ self.request_callback(self._request)
+
+
+class HTTPRequest(object):
+ """A single HTTP request.
+
+ All attributes are type `str` unless otherwise noted.
+
+ .. attribute:: method
+
+ HTTP request method, e.g. "GET" or "POST"
+
+ .. attribute:: uri
+
+ The requested uri.
+
+ .. attribute:: path
+
+ The path portion of `uri`
+
+ .. attribute:: query
+
+ The query portion of `uri`
+
+ .. attribute:: version
+
+ HTTP version specified in request, e.g. "HTTP/1.1"
+
+ .. attribute:: headers
+
+ `HTTPHeader` dictionary-like object for request headers. Acts like
+ a case-insensitive dictionary with additional methods for repeated
+ headers.
+
+ .. attribute:: body
+
+ Request body, if present, as a byte string.
+
+ .. attribute:: remote_ip
+
+ Client's IP address as a string. If `HTTPServer.xheaders` is set,
+ will pass along the real IP address provided by a load balancer
+ in the ``X-Real-Ip`` header
+
+ .. attribute:: protocol
+
+ The protocol used, either "http" or "https". If `HTTPServer.xheaders`
+ is set, will pass along the protocol used by a load balancer if
+ reported via an ``X-Scheme`` header.
+
+ .. attribute:: host
+
+ The requested hostname, usually taken from the ``Host`` header.
+
+ .. attribute:: arguments
+
+ GET/POST arguments are available in the arguments property, which
+ maps arguments names to lists of values (to support multiple values
+ for individual names). Names are of type `str`, while arguments
+ are byte strings. Note that this is different from
+ `RequestHandler.get_argument`, which returns argument values as
+ unicode strings.
+
+ .. attribute:: files
+
+ File uploads are available in the files property, which maps file
+ names to lists of :class:`HTTPFile`.
+
+ .. attribute:: connection
+
+ An HTTP request is attached to a single HTTP connection, which can
+ be accessed through the "connection" attribute. Since connections
+ are typically kept open in HTTP/1.1, multiple requests can be handled
+ sequentially on a single connection.
+ """
+ def __init__(self, method, uri, version="HTTP/1.0", headers=None,
+ body=None, remote_ip=None, protocol=None, host=None,
+ files=None, connection=None):
+ self.method = method
+ self.uri = uri
+ self.version = version
+ self.headers = headers or httputil.HTTPHeaders()
+ self.body = body or ""
+ if connection and connection.xheaders:
+ # Squid uses X-Forwarded-For, others use X-Real-Ip
+ self.remote_ip = self.headers.get(
+ "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
+ if not self._valid_ip(self.remote_ip):
+ self.remote_ip = remote_ip
+ # AWS uses X-Forwarded-Proto
+ self.protocol = self.headers.get(
+ "X-Scheme", self.headers.get("X-Forwarded-Proto", protocol))
+ if self.protocol not in ("http", "https"):
+ self.protocol = "http"
+ else:
+ self.remote_ip = remote_ip
+ if protocol:
+ self.protocol = protocol
+ elif connection and isinstance(connection.stream,
+ iostream.SSLIOStream):
+ self.protocol = "https"
+ else:
+ self.protocol = "http"
+ self.host = host or self.headers.get("Host") or "127.0.0.1"
+ self.files = files or {}
+ self.connection = connection
+ self._start_time = time.time()
+ self._finish_time = None
+
+ scheme, netloc, path, query, fragment = urlparse.urlsplit(native_str(uri))
+ self.path = path
+ self.query = query
+ arguments = parse_qs_bytes(query)
+ self.arguments = {}
+ for name, values in arguments.iteritems():
+ values = [v for v in values if v]
+ if values: self.arguments[name] = values
+
+ def supports_http_1_1(self):
+ """Returns True if this request supports HTTP/1.1 semantics"""
+ return self.version == "HTTP/1.1"
+
+ @property
+ def cookies(self):
+ """A dictionary of Cookie.Morsel objects."""
+ if not hasattr(self, "_cookies"):
+ self._cookies = Cookie.SimpleCookie()
+ if "Cookie" in self.headers:
+ try:
+ self._cookies.load(
+ native_str(self.headers["Cookie"]))
+ except Exception:
+ self._cookies = {}
+ return self._cookies
+
+ def write(self, chunk, callback=None):
+ """Writes the given chunk to the response stream."""
+ assert isinstance(chunk, bytes_type)
+ self.connection.write(chunk, callback=callback)
+
+ def finish(self):
+ """Finishes this HTTP request on the open connection."""
+ self.connection.finish()
+ self._finish_time = time.time()
+
+ def full_url(self):
+ """Reconstructs the full URL for this request."""
+ return self.protocol + "://" + self.host + self.uri
+
+ def request_time(self):
+ """Returns the amount of time it took for this request to execute."""
+ if self._finish_time is None:
+ return time.time() - self._start_time
+ else:
+ return self._finish_time - self._start_time
+
+ def get_ssl_certificate(self):
+ """Returns the client's SSL certificate, if any.
+
+ To use client certificates, the HTTPServer must have been constructed
+ with cert_reqs set in ssl_options, e.g.::
+
+ server = HTTPServer(app,
+ ssl_options=dict(
+ certfile="foo.crt",
+ keyfile="foo.key",
+ cert_reqs=ssl.CERT_REQUIRED,
+ ca_certs="cacert.crt"))
+
+ The return value is a dictionary, see SSLSocket.getpeercert() in
+ the standard library for more details.
+ http://docs.python.org/library/ssl.html#sslsocket-objects
+ """
+ try:
+ return self.connection.stream.socket.getpeercert()
+ except ssl.SSLError:
+ return None
+
+ def __repr__(self):
+ attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
+ "body")
+ args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
+ return "%s(%s, headers=%s)" % (
+ self.__class__.__name__, args, dict(self.headers))
+
+ def _valid_ip(self, ip):
+ try:
+ res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
+ socket.SOCK_STREAM,
+ 0, socket.AI_NUMERICHOST)
+ return bool(res)
+ except socket.gaierror, e:
+ if e.args[0] == socket.EAI_NONAME:
+ return False
+ raise
+ return True
+
diff --git a/tornado/httputil.py b/tornado/httputil.py
new file mode 100644
index 0000000..6bb8719
--- /dev/null
+++ b/tornado/httputil.py
@@ -0,0 +1,279 @@
+#
+# 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.
+
+"""HTTP utility code shared by clients and servers."""
+
+import logging
+import urllib
+import re
+
+from tornado.util import b, ObjectDict
+
+class HTTPHeaders(dict):
+ """A dictionary that maintains Http-Header-Case for all keys.
+
+ Supports multiple values per key via a pair of new methods,
+ add() and get_list(). The regular dictionary interface returns a single
+ value per key, with multiple values joined by a comma.
+
+ >>> h = HTTPHeaders({"content-type": "text/html"})
+ >>> h.keys()
+ ['Content-Type']
+ >>> h["Content-Type"]
+ 'text/html'
+
+ >>> h.add("Set-Cookie", "A=B")
+ >>> h.add("Set-Cookie", "C=D")
+ >>> h["set-cookie"]
+ 'A=B,C=D'
+ >>> h.get_list("set-cookie")
+ ['A=B', 'C=D']
+
+ >>> for (k,v) in sorted(h.get_all()):
+ ... print '%s: %s' % (k,v)
+ ...
+ Content-Type: text/html
+ Set-Cookie: A=B
+ Set-Cookie: C=D
+ """
+ def __init__(self, *args, **kwargs):
+ # Don't pass args or kwargs to dict.__init__, as it will bypass
+ # our __setitem__
+ dict.__init__(self)
+ self._as_list = {}
+ self._last_key = None
+ self.update(*args, **kwargs)
+
+ # new public methods
+
+ def add(self, name, value):
+ """Adds a new value for the given key."""
+ norm_name = HTTPHeaders._normalize_name(name)
+ self._last_key = norm_name
+ if norm_name in self:
+ # bypass our override of __setitem__ since it modifies _as_list
+ dict.__setitem__(self, norm_name, self[norm_name] + ',' + value)
+ self._as_list[norm_name].append(value)
+ else:
+ self[norm_name] = value
+
+ def get_list(self, name):
+ """Returns all values for the given header as a list."""
+ norm_name = HTTPHeaders._normalize_name(name)
+ return self._as_list.get(norm_name, [])
+
+ def get_all(self):
+ """Returns an iterable of all (name, value) pairs.
+
+ If a header has multiple values, multiple pairs will be
+ returned with the same name.
+ """
+ for name, list in self._as_list.iteritems():
+ for value in list:
+ yield (name, value)
+
+ def parse_line(self, line):
+ """Updates the dictionary with a single header line.
+
+ >>> h = HTTPHeaders()
+ >>> h.parse_line("Content-Type: text/html")
+ >>> h.get('content-type')
+ 'text/html'
+ """
+ if line[0].isspace():
+ # continuation of a multi-line header
+ new_part = ' ' + line.lstrip()
+ self._as_list[self._last_key][-1] += new_part
+ dict.__setitem__(self, self._last_key,
+ self[self._last_key] + new_part)
+ else:
+ name, value = line.split(":", 1)
+ self.add(name, value.strip())
+
+ @classmethod
+ def parse(cls, headers):
+ """Returns a dictionary from HTTP header text.
+
+ >>> h = HTTPHeaders.parse("Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
+ >>> sorted(h.iteritems())
+ [('Content-Length', '42'), ('Content-Type', 'text/html')]
+ """
+ h = cls()
+ for line in headers.splitlines():
+ if line:
+ h.parse_line(line)
+ return h
+
+ # dict implementation overrides
+
+ def __setitem__(self, name, value):
+ norm_name = HTTPHeaders._normalize_name(name)
+ dict.__setitem__(self, norm_name, value)
+ self._as_list[norm_name] = [value]
+
+ def __getitem__(self, name):
+ return dict.__getitem__(self, HTTPHeaders._normalize_name(name))
+
+ def __delitem__(self, name):
+ norm_name = HTTPHeaders._normalize_name(name)
+ dict.__delitem__(self, norm_name)
+ del self._as_list[norm_name]
+
+ def __contains__(self, name):
+ norm_name = HTTPHeaders._normalize_name(name)
+ return dict.__contains__(self, norm_name)
+
+ def get(self, name, default=None):
+ return dict.get(self, HTTPHeaders._normalize_name(name), default)
+
+ def update(self, *args, **kwargs):
+ # dict.update bypasses our __setitem__
+ for k, v in dict(*args, **kwargs).iteritems():
+ self[k] = v
+
+ _NORMALIZED_HEADER_RE = re.compile(r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$')
+ _normalized_headers = {}
+
+ @staticmethod
+ def _normalize_name(name):
+ """Converts a name to Http-Header-Case.
+
+ >>> HTTPHeaders._normalize_name("coNtent-TYPE")
+ 'Content-Type'
+ """
+ try:
+ return HTTPHeaders._normalized_headers[name]
+ except KeyError:
+ if HTTPHeaders._NORMALIZED_HEADER_RE.match(name):
+ normalized = name
+ else:
+ normalized = "-".join([w.capitalize() for w in name.split("-")])
+ HTTPHeaders._normalized_headers[name] = normalized
+ return normalized
+
+
+def url_concat(url, args):
+ """Concatenate url and argument dictionary regardless of whether
+ url has existing query parameters.
+
+ >>> url_concat("http://example.com/foo?a=b", dict(c="d"))
+ 'http://example.com/foo?a=b&c=d'
+ """
+ if not args: return url
+ if url[-1] not in ('?', '&'):
+ url += '&' if ('?' in url) else '?'
+ return url + urllib.urlencode(args)
+
+
+class HTTPFile(ObjectDict):
+ """Represents an HTTP file. For backwards compatibility, its instance
+ attributes are also accessible as dictionary keys.
+
+ :ivar filename:
+ :ivar body:
+ :ivar content_type: The content_type comes from the provided HTTP header
+ and should not be trusted outright given that it can be easily forged.
+ """
+ pass
+
+
+def parse_multipart_form_data(boundary, data, arguments, files):
+ """Parses a multipart/form-data body.
+
+ The boundary and data parameters are both byte strings.
+ The dictionaries given in the arguments and files parameters
+ will be updated with the contents of the body.
+ """
+ # The standard allows for the boundary to be quoted in the header,
+ # although it's rare (it happens at least for google app engine
+ # 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
+ # in the wild.
+ if boundary.startswith(b('"')) and boundary.endswith(b('"')):
+ boundary = boundary[1:-1]
+ if data.endswith(b("\r\n")):
+ footer_length = len(boundary) + 6
+ else:
+ footer_length = len(boundary) + 4
+ parts = data[:-footer_length].split(b("--") + boundary + b("\r\n"))
+ for part in parts:
+ if not part: continue
+ eoh = part.find(b("\r\n\r\n"))
+ if eoh == -1:
+ logging.warning("multipart/form-data missing headers")
+ continue
+ headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
+ disp_header = headers.get("Content-Disposition", "")
+ disposition, disp_params = _parse_header(disp_header)
+ if disposition != "form-data" or not part.endswith(b("\r\n")):
+ logging.warning("Invalid multipart/form-data")
+ continue
+ value = part[eoh + 4:-2]
+ if not disp_params.get("name"):
+ logging.warning("multipart/form-data value missing name")
+ continue
+ name = disp_params["name"]
+ if disp_params.get("filename"):
+ ctype = headers.get("Content-Type", "application/unknown")
+ files.setdefault(name, []).append(HTTPFile(
+ filename=disp_params["filename"], body=value,
+ content_type=ctype))
+ else:
+ arguments.setdefault(name, []).append(value)
+
+
+# _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
+# combinations of semicolons and double quotes.
+def _parseparam(s):
+ while s[:1] == ';':
+ s = s[1:]
+ end = s.find(';')
+ while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
+ end = s.find(';', end + 1)
+ if end < 0:
+ end = len(s)
+ f = s[:end]
+ yield f.strip()
+ s = s[end:]
+
+def _parse_header(line):
+ """Parse a Content-type like header.
+
+ Return the main content-type and a dictionary of options.
+
+ """
+ parts = _parseparam(';' + line)
+ key = parts.next()
+ pdict = {}
+ for p in parts:
+ i = p.find('=')
+ if i >= 0:
+ name = p[:i].strip().lower()
+ value = p[i+1:].strip()
+ if len(value) >= 2 and value[0] == value[-1] == '"':
+ value = value[1:-1]
+ value = value.replace('\\\\', '\\').replace('\\"', '"')
+ pdict[name] = value
+ return key, pdict
+
+
+def doctests():
+ import doctest
+ return doctest.DocTestSuite()
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
new file mode 100644
index 0000000..ad46288
--- /dev/null
+++ b/tornado/ioloop.py
@@ -0,0 +1,642 @@
+#
+# 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.
+
+"""An I/O event loop for non-blocking sockets.
+
+Typical applications will use a single `IOLoop` object, in the
+`IOLoop.instance` singleton. The `IOLoop.start` method should usually
+be called at the end of the ``main()`` function. Atypical applications may
+use more than one `IOLoop`, such as one `IOLoop` per thread, or per `unittest`
+case.
+
+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`.
+"""
+
+from __future__ import with_statement
+
+import datetime
+import errno
+import heapq
+import os
+import logging
+import select
+import thread
+import threading
+import time
+import traceback
+
+from tornado import stack_context
+
+try:
+ import signal
+except ImportError:
+ signal = None
+
+from tornado.platform.auto import set_close_exec, Waker
+
+
+class IOLoop(object):
+ """A level-triggered I/O loop.
+
+ We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python
+ 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
+ simultaneous connections, you should use a system that supports either
+ epoll or queue.
+
+ Example usage for a simple TCP server::
+
+ import errno
+ import functools
+ import ioloop
+ import socket
+
+ def connection_ready(sock, fd, events):
+ while True:
+ try:
+ connection, address = sock.accept()
+ except socket.error, e:
+ if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
+ raise
+ return
+ connection.setblocking(0)
+ handle_connection(connection, address)
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.setblocking(0)
+ sock.bind(("", port))
+ sock.listen(128)
+
+ io_loop = ioloop.IOLoop.instance()
+ callback = functools.partial(connection_ready, sock)
+ io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
+ io_loop.start()
+
+ """
+ # Constants from the epoll module
+ _EPOLLIN = 0x001
+ _EPOLLPRI = 0x002
+ _EPOLLOUT = 0x004
+ _EPOLLERR = 0x008
+ _EPOLLHUP = 0x010
+ _EPOLLRDHUP = 0x2000
+ _EPOLLONESHOT = (1 << 30)
+ _EPOLLET = (1 << 31)
+
+ # Our events map exactly to the epoll events
+ NONE = 0
+ READ = _EPOLLIN
+ WRITE = _EPOLLOUT
+ ERROR = _EPOLLERR | _EPOLLHUP
+
+ def __init__(self, impl=None):
+ self._impl = impl or _poll()
+ if hasattr(self._impl, 'fileno'):
+ set_close_exec(self._impl.fileno())
+ self._handlers = {}
+ self._events = {}
+ self._callbacks = []
+ self._callback_lock = threading.Lock()
+ self._timeouts = []
+ self._running = False
+ self._stopped = False
+ self._thread_ident = None
+ self._blocking_signal_threshold = None
+
+ # Create a pipe that we send bogus data to when we want to wake
+ # the I/O loop when it is idle
+ self._waker = Waker()
+ self.add_handler(self._waker.fileno(),
+ lambda fd, events: self._waker.consume(),
+ self.READ)
+
+ @staticmethod
+ def instance():
+ """Returns a global IOLoop instance.
+
+ Most single-threaded applications have a single, global IOLoop.
+ Use this method instead of passing around IOLoop instances
+ throughout your code.
+
+ A common pattern for classes that depend on IOLoops is to use
+ a default argument to enable programs with multiple IOLoops
+ but not require the argument for simpler applications::
+
+ class MyClass(object):
+ def __init__(self, io_loop=None):
+ self.io_loop = io_loop or IOLoop.instance()
+ """
+ if not hasattr(IOLoop, "_instance"):
+ IOLoop._instance = IOLoop()
+ return IOLoop._instance
+
+ @staticmethod
+ def initialized():
+ """Returns true if the singleton instance has been created."""
+ return hasattr(IOLoop, "_instance")
+
+ def install(self):
+ """Installs this IOloop object as the singleton instance.
+
+ This is normally not necessary as `instance()` will create
+ an IOLoop on demand, but you may want to call `install` to use
+ a custom subclass of IOLoop.
+ """
+ assert not IOLoop.initialized()
+ IOLoop._instance = self
+
+ def close(self, all_fds=False):
+ """Closes the IOLoop, freeing any resources used.
+
+ If ``all_fds`` is true, all file descriptors registered on the
+ IOLoop will be closed (not just the ones created by the IOLoop itself.
+ """
+ self.remove_handler(self._waker.fileno())
+ if all_fds:
+ for fd in self._handlers.keys()[:]:
+ try:
+ os.close(fd)
+ except Exception:
+ logging.debug("error closing fd %s", fd, exc_info=True)
+ self._waker.close()
+ self._impl.close()
+
+ def add_handler(self, fd, handler, events):
+ """Registers the given handler to receive the given events for fd."""
+ self._handlers[fd] = stack_context.wrap(handler)
+ self._impl.register(fd, events | self.ERROR)
+
+ def update_handler(self, fd, events):
+ """Changes the events we listen for fd."""
+ self._impl.modify(fd, events | self.ERROR)
+
+ def remove_handler(self, fd):
+ """Stop listening for events on fd."""
+ self._handlers.pop(fd, None)
+ self._events.pop(fd, None)
+ try:
+ self._impl.unregister(fd)
+ except (OSError, IOError):
+ logging.debug("Error deleting fd from IOLoop", exc_info=True)
+
+ def set_blocking_signal_threshold(self, seconds, action):
+ """Sends a signal if the ioloop is blocked for more than s seconds.
+
+ Pass seconds=None to disable. Requires python 2.6 on a unixy
+ platform.
+
+ The action parameter is a python signal handler. Read the
+ documentation for the python 'signal' module for more information.
+ If action is None, the process will be killed if it is blocked for
+ too long.
+ """
+ if not hasattr(signal, "setitimer"):
+ logging.error("set_blocking_signal_threshold requires a signal module "
+ "with the setitimer method")
+ return
+ self._blocking_signal_threshold = seconds
+ if seconds is not None:
+ signal.signal(signal.SIGALRM,
+ action if action is not None else signal.SIG_DFL)
+
+ def set_blocking_log_threshold(self, seconds):
+ """Logs a stack trace if the ioloop is blocked for more than s seconds.
+ Equivalent to set_blocking_signal_threshold(seconds, self.log_stack)
+ """
+ self.set_blocking_signal_threshold(seconds, self.log_stack)
+
+ def log_stack(self, signal, frame):
+ """Signal handler to log the stack trace of the current thread.
+
+ For use with set_blocking_signal_threshold.
+ """
+ logging.warning('IOLoop blocked for %f seconds in\n%s',
+ self._blocking_signal_threshold,
+ ''.join(traceback.format_stack(frame)))
+
+ def start(self):
+ """Starts the I/O loop.
+
+ The loop will run until one of the I/O handlers calls stop(), which
+ will make the loop stop after the current event iteration completes.
+ """
+ if self._stopped:
+ self._stopped = False
+ return
+ self._thread_ident = thread.get_ident()
+ self._running = True
+ while True:
+ poll_timeout = 3600.0
+
+ # Prevent IO event starvation by delaying new callbacks
+ # to the next iteration of the event loop.
+ with self._callback_lock:
+ callbacks = self._callbacks
+ self._callbacks = []
+ for callback in callbacks:
+ self._run_callback(callback)
+
+ if self._timeouts:
+ now = time.time()
+ while self._timeouts:
+ if self._timeouts[0].callback is None:
+ # the timeout was cancelled
+ heapq.heappop(self._timeouts)
+ elif self._timeouts[0].deadline <= now:
+ timeout = heapq.heappop(self._timeouts)
+ self._run_callback(timeout.callback)
+ else:
+ seconds = self._timeouts[0].deadline - now
+ poll_timeout = min(seconds, poll_timeout)
+ break
+
+ if self._callbacks:
+ # If any callbacks or timeouts called add_callback,
+ # we don't want to wait in poll() before we run them.
+ poll_timeout = 0.0
+
+ if not self._running:
+ break
+
+ if self._blocking_signal_threshold is not None:
+ # clear alarm so it doesn't fire while poll is waiting for
+ # events.
+ signal.setitimer(signal.ITIMER_REAL, 0, 0)
+
+ try:
+ event_pairs = self._impl.poll(poll_timeout)
+ except Exception, e:
+ # Depending on python version and IOLoop implementation,
+ # different exception types may be thrown and there are
+ # two ways EINTR might be signaled:
+ # * e.errno == errno.EINTR
+ # * e.args is like (errno.EINTR, 'Interrupted system call')
+ if (getattr(e, 'errno', None) == errno.EINTR or
+ (isinstance(getattr(e, 'args', None), tuple) and
+ len(e.args) == 2 and e.args[0] == errno.EINTR)):
+ continue
+ else:
+ raise
+
+ if self._blocking_signal_threshold is not None:
+ signal.setitimer(signal.ITIMER_REAL,
+ self._blocking_signal_threshold, 0)
+
+ # Pop one fd at a time from the set of pending fds and run
+ # its handler. Since that handler may perform actions on
+ # other file descriptors, there may be reentrant calls to
+ # this IOLoop that update self._events
+ self._events.update(event_pairs)
+ while self._events:
+ fd, events = self._events.popitem()
+ try:
+ self._handlers[fd](fd, events)
+ except (OSError, IOError), e:
+ if e.args[0] == errno.EPIPE:
+ # Happens when the client closes the connection
+ pass
+ else:
+ logging.error("Exception in I/O handler for fd %s",
+ fd, exc_info=True)
+ except Exception:
+ logging.error("Exception in I/O handler for fd %s",
+ fd, exc_info=True)
+ # reset the stopped flag so another start/stop pair can be issued
+ self._stopped = False
+ if self._blocking_signal_threshold is not None:
+ signal.setitimer(signal.ITIMER_REAL, 0, 0)
+
+ def stop(self):
+ """Stop the loop after the current event loop iteration is complete.
+ If the event loop is not currently running, the next call to start()
+ will return immediately.
+
+ To use asynchronous methods from otherwise-synchronous code (such as
+ unit tests), you can start and stop the event loop like this::
+
+ ioloop = IOLoop()
+ async_method(ioloop=ioloop, callback=ioloop.stop)
+ ioloop.start()
+
+ ioloop.start() will return after async_method has run its callback,
+ whether that callback was invoked before or after ioloop.start.
+ """
+ self._running = False
+ self._stopped = True
+ self._waker.wake()
+
+ def running(self):
+ """Returns true if this IOLoop is currently running."""
+ return self._running
+
+ def add_timeout(self, deadline, callback):
+ """Calls the given callback at the time deadline from the I/O loop.
+
+ Returns a handle that may be passed to remove_timeout to cancel.
+
+ ``deadline`` may be a number denoting a unix timestamp (as returned
+ by ``time.time()`` or a ``datetime.timedelta`` object for a deadline
+ relative to the current time.
+
+ Note that it is not safe to call `add_timeout` from other threads.
+ Instead, you must use `add_callback` to transfer control to the
+ IOLoop's thread, and then call `add_timeout` from there.
+ """
+ timeout = _Timeout(deadline, stack_context.wrap(callback))
+ heapq.heappush(self._timeouts, timeout)
+ return timeout
+
+ def remove_timeout(self, timeout):
+ """Cancels a pending timeout.
+
+ The argument is a handle as returned by add_timeout.
+ """
+ # Removing from a heap is complicated, so just leave the defunct
+ # timeout object in the queue (see discussion in
+ # http://docs.python.org/library/heapq.html).
+ # If this turns out to be a problem, we could add a garbage
+ # collection pass whenever there are too many dead timeouts.
+ timeout.callback = None
+
+ def add_callback(self, callback):
+ """Calls the given callback on the next I/O loop iteration.
+
+ It is safe to call this method from any thread at any time.
+ Note that this is the *only* method in IOLoop that makes this
+ guarantee; all other interaction with the IOLoop must be done
+ from that IOLoop's thread. add_callback() may be used to transfer
+ control from other threads to the IOLoop's thread.
+ """
+ with self._callback_lock:
+ list_empty = not self._callbacks
+ self._callbacks.append(stack_context.wrap(callback))
+ if list_empty and thread.get_ident() != self._thread_ident:
+ # 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
+ # empty list, we may need to wake it up (it may wake up on its
+ # own, but an occasional extra wake is harmless). Waking
+ # up a polling IOLoop is relatively expensive, so we try to
+ # avoid it when we can.
+ self._waker.wake()
+
+ def _run_callback(self, callback):
+ try:
+ callback()
+ except Exception:
+ self.handle_callback_exception(callback)
+
+ def handle_callback_exception(self, callback):
+ """This method is called whenever a callback run by the IOLoop
+ throws an exception.
+
+ By default simply logs the exception as an error. Subclasses
+ may override this method to customize reporting of exceptions.
+
+ The exception itself is not passed explicitly, but is available
+ in sys.exc_info.
+ """
+ logging.error("Exception in callback %r", callback, exc_info=True)
+
+
+class _Timeout(object):
+ """An IOLoop timeout, a UNIX timestamp and a callback"""
+
+ # Reduce memory overhead when there are lots of pending callbacks
+ __slots__ = ['deadline', 'callback']
+
+ def __init__(self, deadline, callback):
+ if isinstance(deadline, (int, long, float)):
+ self.deadline = deadline
+ elif isinstance(deadline, datetime.timedelta):
+ self.deadline = time.time() + _Timeout.timedelta_to_seconds(deadline)
+ else:
+ raise TypeError("Unsupported deadline %r" % deadline)
+ self.callback = callback
+
+ @staticmethod
+ def timedelta_to_seconds(td):
+ """Equivalent to td.total_seconds() (introduced in python 2.7)."""
+ return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / float(10**6)
+
+ # Comparison methods to sort by deadline, with object id as a tiebreaker
+ # to guarantee a consistent ordering. The heapq module uses __le__
+ # in python2.5, and __lt__ in 2.6+ (sort() and most other comparisons
+ # use __lt__).
+ def __lt__(self, other):
+ return ((self.deadline, id(self)) <
+ (other.deadline, id(other)))
+
+ def __le__(self, other):
+ return ((self.deadline, id(self)) <=
+ (other.deadline, id(other)))
+
+
+class PeriodicCallback(object):
+ """Schedules the given callback to be called periodically.
+
+ The callback is called every callback_time milliseconds.
+
+ `start` must be called after the PeriodicCallback is created.
+ """
+ def __init__(self, callback, callback_time, io_loop=None):
+ self.callback = callback
+ self.callback_time = callback_time
+ self.io_loop = io_loop or IOLoop.instance()
+ self._running = False
+ self._timeout = None
+
+ def start(self):
+ """Starts the timer."""
+ self._running = True
+ self._next_timeout = time.time()
+ self._schedule_next()
+
+ def stop(self):
+ """Stops the timer."""
+ self._running = False
+ if self._timeout is not None:
+ self.io_loop.remove_timeout(self._timeout)
+ self._timeout = None
+
+ def _run(self):
+ if not self._running: return
+ try:
+ self.callback()
+ except Exception:
+ logging.error("Error in periodic callback", exc_info=True)
+ self._schedule_next()
+
+ def _schedule_next(self):
+ if self._running:
+ current_time = time.time()
+ while self._next_timeout <= current_time:
+ self._next_timeout += self.callback_time / 1000.0
+ self._timeout = self.io_loop.add_timeout(self._next_timeout, self._run)
+
+
+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 _KQueue(object):
+ """A kqueue-based event loop for BSD/Mac systems."""
+ def __init__(self):
+ self._kqueue = select.kqueue()
+ self._active = {}
+
+ def fileno(self):
+ return self._kqueue.fileno()
+
+ def close(self):
+ self._kqueue.close()
+
+ def register(self, fd, events):
+ self._control(fd, events, select.KQ_EV_ADD)
+ self._active[fd] = events
+
+ def modify(self, fd, events):
+ self.unregister(fd)
+ self.register(fd, events)
+
+ def unregister(self, fd):
+ events = self._active.pop(fd)
+ self._control(fd, events, select.KQ_EV_DELETE)
+
+ def _control(self, fd, events, flags):
+ kevents = []
+ if events & IOLoop.WRITE:
+ kevents.append(select.kevent(
+ fd, filter=select.KQ_FILTER_WRITE, flags=flags))
+ if events & IOLoop.READ or not kevents:
+ # Always read when there is not a write
+ kevents.append(select.kevent(
+ fd, filter=select.KQ_FILTER_READ, flags=flags))
+ # 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.
+ for kevent in kevents:
+ self._kqueue.control([kevent], 0)
+
+ def poll(self, timeout):
+ kevents = self._kqueue.control(None, 1000, timeout)
+ events = {}
+ for kevent in kevents:
+ fd = kevent.ident
+ if kevent.filter == select.KQ_FILTER_READ:
+ events[fd] = events.get(fd, 0) | IOLoop.READ
+ if kevent.filter == select.KQ_FILTER_WRITE:
+ if kevent.flags & select.KQ_EV_EOF:
+ # If an asynchronous connection is refused, kqueue
+ # returns a write event with the EOF flag set.
+ # Turn this into an error for consistency with the
+ # other IOLoop implementations.
+ # Note that for read events, EOF may be returned before
+ # all data has been consumed from the socket buffer,
+ # so we only check for EOF on write events.
+ events[fd] = IOLoop.ERROR
+ else:
+ events[fd] = events.get(fd, 0) | IOLoop.WRITE
+ if kevent.flags & select.KQ_EV_ERROR:
+ events[fd] = events.get(fd, 0) | IOLoop.ERROR
+ return events.items()
+
+
+class _Select(object):
+ """A simple, select()-based IOLoop implementation for non-Linux systems"""
+ def __init__(self):
+ self.read_fds = set()
+ self.write_fds = set()
+ self.error_fds = set()
+ self.fd_sets = (self.read_fds, self.write_fds, self.error_fds)
+
+ def close(self):
+ pass
+
+ def register(self, fd, events):
+ if events & IOLoop.READ: self.read_fds.add(fd)
+ if events & IOLoop.WRITE: self.write_fds.add(fd)
+ if events & IOLoop.ERROR:
+ self.error_fds.add(fd)
+ # Closed connections are reported as errors by epoll and kqueue,
+ # but as zero-byte reads by select, so when errors are requested
+ # we need to listen for both read and error.
+ self.read_fds.add(fd)
+
+ def modify(self, fd, events):
+ self.unregister(fd)
+ self.register(fd, events)
+
+ def unregister(self, fd):
+ self.read_fds.discard(fd)
+ self.write_fds.discard(fd)
+ self.error_fds.discard(fd)
+
+ def poll(self, timeout):
+ readable, writeable, errors = select.select(
+ self.read_fds, self.write_fds, self.error_fds, timeout)
+ events = {}
+ for fd in readable:
+ events[fd] = events.get(fd, 0) | IOLoop.READ
+ for fd in writeable:
+ events[fd] = events.get(fd, 0) | IOLoop.WRITE
+ for fd in errors:
+ events[fd] = events.get(fd, 0) | IOLoop.ERROR
+ return events.items()
+
+
+# Choose a poll implementation. Use epoll if it is available, fall back to
+# select() for non-Linux platforms
+if hasattr(select, "epoll"):
+ # Python 2.6+ on Linux
+ _poll = select.epoll
+elif hasattr(select, "kqueue"):
+ # Python 2.6+ on BSD or Mac
+ _poll = _KQueue
+else:
+ try:
+ # Linux systems with our C module installed
+ import epoll
+ _poll = _EPoll
+ except Exception:
+ # All other systems
+ import sys
+ if "linux" in sys.platform:
+ logging.warning("epoll module not found; using select()")
+ _poll = _Select
diff --git a/tornado/iostream.py b/tornado/iostream.py
new file mode 100644
index 0000000..88c5a26
--- /dev/null
+++ b/tornado/iostream.py
@@ -0,0 +1,727 @@
+#
+# 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.
+
+"""A utility class to write to and read from a non-blocking socket."""
+
+from __future__ import with_statement
+
+import collections
+import errno
+import logging
+import socket
+import sys
+import re
+
+from tornado import ioloop
+from tornado import stack_context
+from tornado.util import b, bytes_type
+
+try:
+ import ssl # Python 2.6+
+except ImportError:
+ ssl = None
+
+class IOStream(object):
+ r"""A utility class to write to and read from a non-blocking socket.
+
+ We support a non-blocking ``write()`` and a family of ``read_*()`` methods.
+ All of the methods take callbacks (since writing and reading are
+ non-blocking and asynchronous).
+
+ The socket parameter may either be connected or unconnected. For
+ server operations the socket is the result of calling socket.accept().
+ For client operations the socket is created with socket.socket(),
+ and may either be connected before passing it to the IOStream or
+ connected with IOStream.connect.
+
+ A very simple (and broken) HTTP client using this class::
+
+ from tornado import ioloop
+ from tornado import iostream
+ import socket
+
+ def send_request():
+ stream.write("GET / HTTP/1.0\r\nHost: friendfeed.com\r\n\r\n")
+ stream.read_until("\r\n\r\n", on_headers)
+
+ def on_headers(data):
+ headers = {}
+ for line in data.split("\r\n"):
+ parts = line.split(":")
+ if len(parts) == 2:
+ headers[parts[0].strip()] = parts[1].strip()
+ stream.read_bytes(int(headers["Content-Length"]), on_body)
+
+ def on_body(data):
+ print data
+ stream.close()
+ ioloop.IOLoop.instance().stop()
+
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
+ stream = iostream.IOStream(s)
+ stream.connect(("friendfeed.com", 80), send_request)
+ ioloop.IOLoop.instance().start()
+
+ """
+ def __init__(self, socket, io_loop=None, max_buffer_size=104857600,
+ read_chunk_size=4096):
+ self.socket = socket
+ self.socket.setblocking(False)
+ self.io_loop = io_loop or ioloop.IOLoop.instance()
+ self.max_buffer_size = max_buffer_size
+ self.read_chunk_size = read_chunk_size
+ self._read_buffer = collections.deque()
+ self._write_buffer = collections.deque()
+ self._read_buffer_size = 0
+ self._write_buffer_frozen = False
+ self._read_delimiter = None
+ self._read_regex = None
+ self._read_bytes = None
+ self._read_until_close = False
+ self._read_callback = None
+ self._streaming_callback = None
+ self._write_callback = None
+ self._close_callback = None
+ self._connect_callback = None
+ self._connecting = False
+ self._state = None
+ self._pending_callbacks = 0
+
+ def connect(self, address, callback=None):
+ """Connects the socket to a remote address without blocking.
+
+ May only be called if the socket passed to the constructor was
+ not previously connected. The address parameter is in the
+ same format as for socket.connect, i.e. a (host, port) tuple.
+ If callback is specified, it will be called when the
+ connection is completed.
+
+ Note that it is safe to call IOStream.write while the
+ connection is pending, in which case the data will be written
+ as soon as the connection is ready. Calling IOStream read
+ methods before the socket is connected works on some platforms
+ but is non-portable.
+ """
+ self._connecting = True
+ try:
+ self.socket.connect(address)
+ except socket.error, e:
+ # In non-blocking mode we expect connect() to raise an
+ # exception with EINPROGRESS or EWOULDBLOCK.
+ #
+ # On freebsd, other errors such as ECONNREFUSED may be
+ # returned immediately when attempting to connect to
+ # localhost, so handle them the same way as an error
+ # reported later in _handle_connect.
+ if e.args[0] not in (errno.EINPROGRESS, errno.EWOULDBLOCK):
+ logging.warning("Connect error on fd %d: %s",
+ self.socket.fileno(), e)
+ self.close()
+ return
+ self._connect_callback = stack_context.wrap(callback)
+ self._add_io_state(self.io_loop.WRITE)
+
+ def read_until_regex(self, regex, callback):
+ """Call callback when we read the given regex pattern."""
+ assert not self._read_callback, "Already reading"
+ self._read_regex = re.compile(regex)
+ self._read_callback = stack_context.wrap(callback)
+ while True:
+ # See if we've already got the data from a previous read
+ if self._read_from_buffer():
+ return
+ self._check_closed()
+ if self._read_to_buffer() == 0:
+ break
+ self._add_io_state(self.io_loop.READ)
+
+ def read_until(self, delimiter, callback):
+ """Call callback when we read the given delimiter."""
+ assert not self._read_callback, "Already reading"
+ self._read_delimiter = delimiter
+ self._read_callback = stack_context.wrap(callback)
+ while True:
+ # See if we've already got the data from a previous read
+ if self._read_from_buffer():
+ return
+ self._check_closed()
+ if self._read_to_buffer() == 0:
+ break
+ self._add_io_state(self.io_loop.READ)
+
+ def read_bytes(self, num_bytes, callback, streaming_callback=None):
+ """Call callback when we read the given number of bytes.
+
+ If a ``streaming_callback`` is given, it will be called with chunks
+ of data as they become available, and the argument to the final
+ ``callback`` will be empty.
+ """
+ assert not self._read_callback, "Already reading"
+ assert isinstance(num_bytes, (int, long))
+ self._read_bytes = num_bytes
+ self._read_callback = stack_context.wrap(callback)
+ self._streaming_callback = stack_context.wrap(streaming_callback)
+ while True:
+ if self._read_from_buffer():
+ return
+ self._check_closed()
+ if self._read_to_buffer() == 0:
+ break
+ self._add_io_state(self.io_loop.READ)
+
+ def read_until_close(self, callback, streaming_callback=None):
+ """Reads all data from the socket until it is closed.
+
+ If a ``streaming_callback`` is given, it will be called with chunks
+ of data as they become available, and the argument to the final
+ ``callback`` will be empty.
+
+ Subject to ``max_buffer_size`` limit from `IOStream` constructor if
+ a ``streaming_callback`` is not used.
+ """
+ assert not self._read_callback, "Already reading"
+ if self.closed():
+ self._run_callback(callback, self._consume(self._read_buffer_size))
+ return
+ self._read_until_close = True
+ self._read_callback = stack_context.wrap(callback)
+ self._streaming_callback = stack_context.wrap(streaming_callback)
+ self._add_io_state(self.io_loop.READ)
+
+ def write(self, data, callback=None):
+ """Write the given data to this stream.
+
+ If callback is given, we call it when all of the buffered write
+ data has been successfully written to the stream. If there was
+ previously buffered write data and an old write callback, that
+ callback is simply overwritten with this new callback.
+ """
+ assert isinstance(data, bytes_type)
+ self._check_closed()
+ if data:
+ # We use bool(_write_buffer) as a proxy for write_buffer_size>0,
+ # so never put empty strings in the buffer.
+ self._write_buffer.append(data)
+ self._write_callback = stack_context.wrap(callback)
+ self._handle_write()
+ if self._write_buffer:
+ self._add_io_state(self.io_loop.WRITE)
+ self._maybe_add_error_listener()
+
+ def set_close_callback(self, callback):
+ """Call the given callback when the stream is closed."""
+ self._close_callback = stack_context.wrap(callback)
+
+ def close(self):
+ """Close this stream."""
+ if self.socket is not None:
+ if self._read_until_close:
+ callback = self._read_callback
+ self._read_callback = None
+ self._read_until_close = False
+ self._run_callback(callback,
+ self._consume(self._read_buffer_size))
+ if self._state is not None:
+ self.io_loop.remove_handler(self.socket.fileno())
+ self._state = None
+ self.socket.close()
+ self.socket = None
+ if self._close_callback and self._pending_callbacks == 0:
+ # if there are pending callbacks, don't run the close callback
+ # until they're done (see _maybe_add_error_handler)
+ cb = self._close_callback
+ self._close_callback = None
+ self._run_callback(cb)
+
+ def reading(self):
+ """Returns true if we are currently reading from the stream."""
+ return self._read_callback is not None
+
+ def writing(self):
+ """Returns true if we are currently writing to the stream."""
+ return bool(self._write_buffer)
+
+ def closed(self):
+ """Returns true if the stream has been closed."""
+ return self.socket is None
+
+ def _handle_events(self, fd, events):
+ if not self.socket:
+ logging.warning("Got events for closed stream %d", fd)
+ return
+ try:
+ if events & self.io_loop.READ:
+ self._handle_read()
+ if not self.socket:
+ return
+ if events & self.io_loop.WRITE:
+ if self._connecting:
+ self._handle_connect()
+ self._handle_write()
+ if not self.socket:
+ return
+ if events & self.io_loop.ERROR:
+ # We may have queued up a user callback in _handle_read or
+ # _handle_write, so don't close the IOStream until those
+ # callbacks have had a chance to run.
+ self.io_loop.add_callback(self.close)
+ return
+ state = self.io_loop.ERROR
+ if self.reading():
+ state |= self.io_loop.READ
+ if self.writing():
+ state |= self.io_loop.WRITE
+ if state == self.io_loop.ERROR:
+ state |= self.io_loop.READ
+ if state != self._state:
+ assert self._state is not None, \
+ "shouldn't happen: _handle_events without self._state"
+ self._state = state
+ self.io_loop.update_handler(self.socket.fileno(), self._state)
+ except Exception:
+ logging.error("Uncaught exception, closing connection.",
+ exc_info=True)
+ self.close()
+ raise
+
+ def _run_callback(self, callback, *args):
+ def wrapper():
+ self._pending_callbacks -= 1
+ try:
+ callback(*args)
+ except Exception:
+ logging.error("Uncaught exception, closing connection.",
+ exc_info=True)
+ # Close the socket on an uncaught exception from a user callback
+ # (It would eventually get closed when the socket object is
+ # gc'd, but we don't want to rely on gc happening before we
+ # run out of file descriptors)
+ self.close()
+ # Re-raise the exception so that IOLoop.handle_callback_exception
+ # can see it and log the error
+ raise
+ self._maybe_add_error_listener()
+ # We schedule callbacks to be run on the next IOLoop iteration
+ # rather than running them directly for several reasons:
+ # * Prevents unbounded stack growth when a callback calls an
+ # IOLoop operation that immediately runs another callback
+ # * Provides a predictable execution context for e.g.
+ # non-reentrant mutexes
+ # * Ensures that the try/except in wrapper() is run outside
+ # of the application's StackContexts
+ with stack_context.NullContext():
+ # stack_context was already captured in callback, we don't need to
+ # capture it again for IOStream's wrapper. This is especially
+ # important if the callback was pre-wrapped before entry to
+ # IOStream (as in HTTPConnection._header_callback), as we could
+ # capture and leak the wrong context here.
+ self._pending_callbacks += 1
+ self.io_loop.add_callback(wrapper)
+
+ def _handle_read(self):
+ while True:
+ try:
+ # Read from the socket until we get EWOULDBLOCK or equivalent.
+ # SSL sockets do some internal buffering, and if the data is
+ # sitting in the SSL object's buffer select() and friends
+ # can't see it; the only way to find out if it's there is to
+ # try to read it.
+ result = self._read_to_buffer()
+ except Exception:
+ self.close()
+ return
+ if result == 0:
+ break
+ else:
+ if self._read_from_buffer():
+ return
+
+ def _read_from_socket(self):
+ """Attempts to read from the socket.
+
+ Returns the data read or None if there is nothing to read.
+ May be overridden in subclasses.
+ """
+ try:
+ chunk = self.socket.recv(self.read_chunk_size)
+ except socket.error, e:
+ if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ return None
+ else:
+ raise
+ if not chunk:
+ self.close()
+ return None
+ return chunk
+
+ def _read_to_buffer(self):
+ """Reads from the socket and appends the result to the read buffer.
+
+ Returns the number of bytes read. Returns 0 if there is nothing
+ to read (i.e. the read returns EWOULDBLOCK or equivalent). On
+ error closes the socket and raises an exception.
+ """
+ try:
+ chunk = self._read_from_socket()
+ except socket.error, e:
+ # ssl.SSLError is a subclass of socket.error
+ logging.warning("Read error on %d: %s",
+ self.socket.fileno(), e)
+ self.close()
+ raise
+ if chunk is None:
+ return 0
+ self._read_buffer.append(chunk)
+ self._read_buffer_size += len(chunk)
+ if self._read_buffer_size >= self.max_buffer_size:
+ logging.error("Reached maximum read buffer size")
+ self.close()
+ raise IOError("Reached maximum read buffer size")
+ return len(chunk)
+
+ def _read_from_buffer(self):
+ """Attempts to complete the currently-pending read from the buffer.
+
+ Returns True if the read was completed.
+ """
+ if self._read_bytes is not None:
+ if self._streaming_callback is not None and self._read_buffer_size:
+ bytes_to_consume = min(self._read_bytes, self._read_buffer_size)
+ self._read_bytes -= bytes_to_consume
+ self._run_callback(self._streaming_callback,
+ self._consume(bytes_to_consume))
+ if self._read_buffer_size >= self._read_bytes:
+ num_bytes = self._read_bytes
+ callback = self._read_callback
+ self._read_callback = None
+ self._streaming_callback = None
+ self._read_bytes = None
+ self._run_callback(callback, self._consume(num_bytes))
+ return True
+ elif self._read_delimiter is not None:
+ # Multi-byte delimiters (e.g. '\r\n') may straddle two
+ # chunks in the read buffer, so we can't easily find them
+ # without collapsing the buffer. However, since protocols
+ # using delimited reads (as opposed to reads of a known
+ # length) tend to be "line" oriented, the delimiter is likely
+ # to be in the first few chunks. Merge the buffer gradually
+ # since large merges are relatively expensive and get undone in
+ # consume().
+ loc = -1
+ if self._read_buffer:
+ loc = self._read_buffer[0].find(self._read_delimiter)
+ while loc == -1 and len(self._read_buffer) > 1:
+ # Grow by doubling, but don't split the second chunk just
+ # because the first one is small.
+ new_len = max(len(self._read_buffer[0]) * 2,
+ (len(self._read_buffer[0]) +
+ len(self._read_buffer[1])))
+ _merge_prefix(self._read_buffer, new_len)
+ loc = self._read_buffer[0].find(self._read_delimiter)
+ if loc != -1:
+ callback = self._read_callback
+ delimiter_len = len(self._read_delimiter)
+ self._read_callback = None
+ self._streaming_callback = None
+ self._read_delimiter = None
+ self._run_callback(callback,
+ self._consume(loc + delimiter_len))
+ return True
+ elif self._read_regex is not None:
+ m = None
+ if self._read_buffer:
+ m = self._read_regex.search(self._read_buffer[0])
+ while m is None and len(self._read_buffer) > 1:
+ # Grow by doubling, but don't split the second chunk just
+ # because the first one is small.
+ new_len = max(len(self._read_buffer[0]) * 2,
+ (len(self._read_buffer[0]) +
+ len(self._read_buffer[1])))
+ _merge_prefix(self._read_buffer, new_len)
+ m = self._read_regex.search(self._read_buffer[0])
+ _merge_prefix(self._read_buffer, sys.maxint)
+ m = self._read_regex.search(self._read_buffer[0])
+ if m:
+ callback = self._read_callback
+ self._read_callback = None
+ self._streaming_callback = None
+ self._read_regex = None
+ self._run_callback(callback, self._consume(m.end()))
+ return True
+ elif self._read_until_close:
+ if self._streaming_callback is not None and self._read_buffer_size:
+ self._run_callback(self._streaming_callback,
+ self._consume(self._read_buffer_size))
+ return False
+
+ def _handle_connect(self):
+ err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
+ if err != 0:
+ # IOLoop implementations may vary: some of them return
+ # an error state before the socket becomes writable, so
+ # in that case a connection failure would be handled by the
+ # error path in _handle_events instead of here.
+ logging.warning("Connect error on fd %d: %s",
+ self.socket.fileno(), errno.errorcode[err])
+ self.close()
+ return
+ if self._connect_callback is not None:
+ callback = self._connect_callback
+ self._connect_callback = None
+ self._run_callback(callback)
+ self._connecting = False
+
+ def _handle_write(self):
+ while self._write_buffer:
+ try:
+ if not self._write_buffer_frozen:
+ # On windows, socket.send blows up if given a
+ # write buffer that's too large, instead of just
+ # returning the number of bytes it was able to
+ # process. Therefore we must not call socket.send
+ # with more than 128KB at a time.
+ _merge_prefix(self._write_buffer, 128 * 1024)
+ num_bytes = self.socket.send(self._write_buffer[0])
+ if num_bytes == 0:
+ # With OpenSSL, if we couldn't write the entire buffer,
+ # the very same string object must be used on the
+ # next call to send. Therefore we suppress
+ # merging the write buffer after an incomplete send.
+ # A cleaner solution would be to set
+ # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER, but this is
+ # not yet accessible from python
+ # (http://bugs.python.org/issue8240)
+ self._write_buffer_frozen = True
+ break
+ self._write_buffer_frozen = False
+ _merge_prefix(self._write_buffer, num_bytes)
+ self._write_buffer.popleft()
+ except socket.error, e:
+ if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ self._write_buffer_frozen = True
+ break
+ else:
+ logging.warning("Write error on %d: %s",
+ self.socket.fileno(), e)
+ self.close()
+ return
+ if not self._write_buffer and self._write_callback:
+ callback = self._write_callback
+ self._write_callback = None
+ self._run_callback(callback)
+
+ def _consume(self, loc):
+ if loc == 0:
+ return b("")
+ _merge_prefix(self._read_buffer, loc)
+ self._read_buffer_size -= loc
+ return self._read_buffer.popleft()
+
+ def _check_closed(self):
+ if not self.socket:
+ raise IOError("Stream is closed")
+
+ def _maybe_add_error_listener(self):
+ if self._state is None and self._pending_callbacks == 0:
+ if self.socket is None:
+ cb = self._close_callback
+ if cb is not None:
+ self._close_callback = None
+ self._run_callback(cb)
+ else:
+ self._add_io_state(ioloop.IOLoop.READ)
+
+ def _add_io_state(self, state):
+ """Adds `state` (IOLoop.{READ,WRITE} flags) to our event handler.
+
+ Implementation notes: Reads and writes have a fast path and a
+ slow path. The fast path reads synchronously from socket
+ buffers, while the slow path uses `_add_io_state` to schedule
+ an IOLoop callback. Note that in both cases, the callback is
+ run asynchronously with `_run_callback`.
+
+ To detect closed connections, we must have called
+ `_add_io_state` at some point, but we want to delay this as
+ much as possible so we don't have to set an `IOLoop.ERROR`
+ listener that will be overwritten by the next slow-path
+ operation. As long as there are callbacks scheduled for
+ fast-path ops, those callbacks may do more reads.
+ If a sequence of fast-path ops do not end in a slow-path op,
+ (e.g. for an @asynchronous long-poll request), we must add
+ the error handler. This is done in `_run_callback` and `write`
+ (since the write callback is optional so we can have a
+ fast-path write with no `_run_callback`)
+ """
+ if self.socket is None:
+ # connection has been closed, so there can be no future events
+ return
+ if self._state is None:
+ self._state = ioloop.IOLoop.ERROR | state
+ with stack_context.NullContext():
+ self.io_loop.add_handler(
+ self.socket.fileno(), self._handle_events, self._state)
+ elif not self._state & state:
+ self._state = self._state | state
+ self.io_loop.update_handler(self.socket.fileno(), self._state)
+
+
+class SSLIOStream(IOStream):
+ """A utility class to write to and read from a non-blocking SSL socket.
+
+ If the socket passed to the constructor is already connected,
+ it should be wrapped with::
+
+ ssl.wrap_socket(sock, do_handshake_on_connect=False, **kwargs)
+
+ before constructing the SSLIOStream. Unconnected sockets will be
+ wrapped when IOStream.connect is finished.
+ """
+ def __init__(self, *args, **kwargs):
+ """Creates an SSLIOStream.
+
+ If a dictionary is provided as keyword argument ssl_options,
+ it will be used as additional keyword arguments to ssl.wrap_socket.
+ """
+ self._ssl_options = kwargs.pop('ssl_options', {})
+ super(SSLIOStream, self).__init__(*args, **kwargs)
+ self._ssl_accepting = True
+ self._handshake_reading = False
+ self._handshake_writing = False
+
+ def reading(self):
+ return self._handshake_reading or super(SSLIOStream, self).reading()
+
+ def writing(self):
+ return self._handshake_writing or super(SSLIOStream, self).writing()
+
+ def _do_ssl_handshake(self):
+ # Based on code from test_ssl.py in the python stdlib
+ try:
+ self._handshake_reading = False
+ self._handshake_writing = False
+ self.socket.do_handshake()
+ except ssl.SSLError, err:
+ if err.args[0] == ssl.SSL_ERROR_WANT_READ:
+ self._handshake_reading = True
+ return
+ elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE:
+ self._handshake_writing = True
+ return
+ elif err.args[0] in (ssl.SSL_ERROR_EOF,
+ ssl.SSL_ERROR_ZERO_RETURN):
+ return self.close()
+ elif err.args[0] == ssl.SSL_ERROR_SSL:
+ logging.warning("SSL Error on %d: %s", self.socket.fileno(), err)
+ return self.close()
+ raise
+ except socket.error, err:
+ if err.args[0] == errno.ECONNABORTED:
+ return self.close()
+ else:
+ self._ssl_accepting = False
+ super(SSLIOStream, self)._handle_connect()
+
+ def _handle_read(self):
+ if self._ssl_accepting:
+ self._do_ssl_handshake()
+ return
+ super(SSLIOStream, self)._handle_read()
+
+ def _handle_write(self):
+ if self._ssl_accepting:
+ self._do_ssl_handshake()
+ return
+ super(SSLIOStream, self)._handle_write()
+
+ def _handle_connect(self):
+ self.socket = ssl.wrap_socket(self.socket,
+ do_handshake_on_connect=False,
+ **self._ssl_options)
+ # Don't call the superclass's _handle_connect (which is responsible
+ # for telling the application that the connection is complete)
+ # until we've completed the SSL handshake (so certificates are
+ # available, etc).
+
+
+ def _read_from_socket(self):
+ if self._ssl_accepting:
+ # If the handshake hasn't finished yet, there can't be anything
+ # to read (attempting to read may or may not raise an exception
+ # depending on the SSL version)
+ return None
+ try:
+ # SSLSocket objects have both a read() and recv() method,
+ # while regular sockets only have recv().
+ # The recv() method blocks (at least in python 2.6) if it is
+ # called when there is nothing to read, so we have to use
+ # read() instead.
+ chunk = self.socket.read(self.read_chunk_size)
+ except ssl.SSLError, e:
+ # SSLError is a subclass of socket.error, so this except
+ # block must come first.
+ if e.args[0] == ssl.SSL_ERROR_WANT_READ:
+ return None
+ else:
+ raise
+ except socket.error, e:
+ if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ return None
+ else:
+ raise
+ if not chunk:
+ self.close()
+ return None
+ return chunk
+
+def _merge_prefix(deque, size):
+ """Replace the first entries in a deque of strings with a single
+ string of up to size bytes.
+
+ >>> d = collections.deque(['abc', 'de', 'fghi', 'j'])
+ >>> _merge_prefix(d, 5); print d
+ deque(['abcde', 'fghi', 'j'])
+
+ Strings will be split as necessary to reach the desired size.
+ >>> _merge_prefix(d, 7); print d
+ deque(['abcdefg', 'hi', 'j'])
+
+ >>> _merge_prefix(d, 3); print d
+ deque(['abc', 'defg', 'hi', 'j'])
+
+ >>> _merge_prefix(d, 100); print d
+ deque(['abcdefghij'])
+ """
+ if len(deque) == 1 and len(deque[0]) <= size:
+ return
+ prefix = []
+ remaining = size
+ while deque and remaining > 0:
+ chunk = deque.popleft()
+ if len(chunk) > remaining:
+ deque.appendleft(chunk[remaining:])
+ chunk = chunk[:remaining]
+ prefix.append(chunk)
+ remaining -= len(chunk)
+ # This data structure normally just contains byte strings, but
+ # the unittest gets messy if it doesn't use the default str() type,
+ # so do the merge based on the type of data that's actually present.
+ if prefix:
+ deque.appendleft(type(prefix[0])().join(prefix))
+ if not deque:
+ deque.appendleft(b(""))
+
+def doctests():
+ import doctest
+ return doctest.DocTestSuite()
diff --git a/tornado/locale.py b/tornado/locale.py
new file mode 100644
index 0000000..ab78f74
--- /dev/null
+++ b/tornado/locale.py
@@ -0,0 +1,471 @@
+#
+# 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.
+
+"""Translation methods for generating localized strings.
+
+To load a locale and generate a translated string::
+
+ user_locale = locale.get("es_LA")
+ print user_locale.translate("Sign out")
+
+locale.get() returns the closest matching locale, not necessarily the
+specific locale you requested. You can support pluralization with
+additional arguments to translate(), e.g.::
+
+ people = [...]
+ message = user_locale.translate(
+ "%(list)s is online", "%(list)s are online", len(people))
+ print message % {"list": user_locale.list(people)}
+
+The first string is chosen if len(people) == 1, otherwise the second
+string is chosen.
+
+Applications should call one of load_translations (which uses a simple
+CSV format) or load_gettext_translations (which uses the .mo format
+supported by gettext and related tools). If neither method is called,
+the locale.translate method will simply return the original string.
+"""
+
+import csv
+import datetime
+import logging
+import os
+import re
+
+_default_locale = "en_US"
+_translations = {}
+_supported_locales = frozenset([_default_locale])
+_use_gettext = False
+
+def get(*locale_codes):
+ """Returns the closest match for the given locale codes.
+
+ We iterate over all given locale codes in order. If we have a tight
+ or a loose match for the code (e.g., "en" for "en_US"), we return
+ the locale. Otherwise we move to the next code in the list.
+
+ By default we return en_US if no translations are found for any of
+ the specified locales. You can change the default locale with
+ set_default_locale() below.
+ """
+ return Locale.get_closest(*locale_codes)
+
+
+def set_default_locale(code):
+ """Sets the default locale, used in get_closest_locale().
+
+ The default locale is assumed to be the language used for all strings
+ in the system. The translations loaded from disk are mappings from
+ the default locale to the destination locale. Consequently, you don't
+ need to create a translation file for the default locale.
+ """
+ global _default_locale
+ global _supported_locales
+ _default_locale = code
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+
+
+def load_translations(directory):
+ u"""Loads translations from CSV files in a directory.
+
+ Translations are strings with optional Python-style named placeholders
+ (e.g., "My name is %(name)s") and their associated translations.
+
+ The directory should have translation files of the form LOCALE.csv,
+ e.g. es_GT.csv. The CSV files should have two or three columns: string,
+ translation, and an optional plural indicator. Plural indicators should
+ be one of "plural" or "singular". A given string can have both singular
+ and plural forms. For example "%(name)s liked this" may have a
+ different verb conjugation depending on whether %(name)s is one
+ name or a list of names. There should be two rows in the CSV file for
+ that string, one with plural indicator "singular", and one "plural".
+ For strings with no verbs that would change on translation, simply
+ use "unknown" or the empty string (or don't include the column at all).
+
+ The file is read using the csv module in the default "excel" dialect.
+ In this format there should not be spaces after the commas.
+
+ Example translation es_LA.csv:
+
+ "I love you","Te amo"
+ "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural"
+ "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular"
+
+ """
+ global _translations
+ global _supported_locales
+ _translations = {}
+ for path in os.listdir(directory):
+ if not path.endswith(".csv"): continue
+ locale, extension = path.split(".")
+ if not re.match("[a-z]+(_[A-Z]+)?$", locale):
+ logging.error("Unrecognized locale %r (path: %s)", locale,
+ os.path.join(directory, path))
+ continue
+ f = open(os.path.join(directory, path), "r")
+ _translations[locale] = {}
+ for i, row in enumerate(csv.reader(f)):
+ if not row or len(row) < 2: continue
+ row = [c.decode("utf-8").strip() for c in row]
+ english, translation = row[:2]
+ if len(row) > 2:
+ plural = row[2] or "unknown"
+ else:
+ plural = "unknown"
+ if plural not in ("plural", "singular", "unknown"):
+ logging.error("Unrecognized plural indicator %r in %s line %d",
+ plural, path, i + 1)
+ continue
+ _translations[locale].setdefault(plural, {})[english] = translation
+ f.close()
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+ logging.info("Supported locales: %s", sorted(_supported_locales))
+
+def load_gettext_translations(directory, domain):
+ """Loads translations from gettext's locale tree
+
+ Locale tree is similar to system's /usr/share/locale, like:
+
+ {directory}/{lang}/LC_MESSAGES/{domain}.mo
+
+ Three steps are required to have you app translated:
+
+ 1. Generate POT translation file
+ xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py file2.html etc
+
+ 2. Merge against existing POT file:
+ msgmerge old.po cyclone.po > new.po
+
+ 3. Compile:
+ msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
+ """
+ import gettext
+ global _translations
+ global _supported_locales
+ global _use_gettext
+ _translations = {}
+ for lang in os.listdir(directory):
+ if lang.startswith('.'): continue # skip .svn, etc
+ if os.path.isfile(os.path.join(directory, lang)): continue
+ try:
+ os.stat(os.path.join(directory, lang, "LC_MESSAGES", domain+".mo"))
+ _translations[lang] = gettext.translation(domain, directory,
+ languages=[lang])
+ except Exception, e:
+ logging.error("Cannot load translation for '%s': %s", lang, str(e))
+ continue
+ _supported_locales = frozenset(_translations.keys() + [_default_locale])
+ _use_gettext = True
+ logging.info("Supported locales: %s", sorted(_supported_locales))
+
+
+def get_supported_locales(cls):
+ """Returns a list of all the supported locale codes."""
+ return _supported_locales
+
+
+class Locale(object):
+ """Object representing a locale.
+
+ After calling one of `load_translations` or `load_gettext_translations`,
+ call `get` or `get_closest` to get a Locale object.
+ """
+ @classmethod
+ def get_closest(cls, *locale_codes):
+ """Returns the closest match for the given locale code."""
+ for code in locale_codes:
+ if not code: continue
+ code = code.replace("-", "_")
+ parts = code.split("_")
+ if len(parts) > 2:
+ continue
+ elif len(parts) == 2:
+ code = parts[0].lower() + "_" + parts[1].upper()
+ if code in _supported_locales:
+ return cls.get(code)
+ if parts[0].lower() in _supported_locales:
+ return cls.get(parts[0].lower())
+ return cls.get(_default_locale)
+
+ @classmethod
+ def get(cls, code):
+ """Returns the Locale for the given locale code.
+
+ If it is not supported, we raise an exception.
+ """
+ if not hasattr(cls, "_cache"):
+ cls._cache = {}
+ if code not in cls._cache:
+ assert code in _supported_locales
+ translations = _translations.get(code, None)
+ if translations is None:
+ locale = CSVLocale(code, {})
+ elif _use_gettext:
+ locale = GettextLocale(code, translations)
+ else:
+ locale = CSVLocale(code, translations)
+ cls._cache[code] = locale
+ return cls._cache[code]
+
+ def __init__(self, code, translations):
+ self.code = code
+ self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
+ self.rtl = False
+ for prefix in ["fa", "ar", "he"]:
+ if self.code.startswith(prefix):
+ self.rtl = True
+ break
+ self.translations = translations
+
+ # Initialize strings for date formatting
+ _ = self.translate
+ self._months = [
+ _("January"), _("February"), _("March"), _("April"),
+ _("May"), _("June"), _("July"), _("August"),
+ _("September"), _("October"), _("November"), _("December")]
+ self._weekdays = [
+ _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
+ _("Friday"), _("Saturday"), _("Sunday")]
+
+ def translate(self, message, plural_message=None, count=None):
+ """Returns the translation for the given message for this locale.
+
+ If plural_message is given, you must also provide count. We return
+ plural_message when count != 1, and we return the singular form
+ for the given message when count == 1.
+ """
+ raise NotImplementedError()
+
+ def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
+ full_format=False):
+ """Formats the given date (which should be GMT).
+
+ By default, we return a relative time (e.g., "2 minutes ago"). You
+ can return an absolute date string with relative=False.
+
+ You can force a full format date ("July 10, 1980") with
+ full_format=True.
+
+ This method is primarily intended for dates in the past.
+ For dates in the future, we fall back to full format.
+ """
+ if self.code.startswith("ru"):
+ relative = False
+ if type(date) in (int, long, float):
+ date = datetime.datetime.utcfromtimestamp(date)
+ now = datetime.datetime.utcnow()
+ if date > now:
+ if relative and (date - now).seconds < 60:
+ # Due to click skew, things are some things slightly
+ # in the future. Round timestamps in the immediate
+ # future down to now in relative mode.
+ date = now
+ else:
+ # Otherwise, future dates always use the full format.
+ full_format = True
+ local_date = date - datetime.timedelta(minutes=gmt_offset)
+ local_now = now - datetime.timedelta(minutes=gmt_offset)
+ local_yesterday = local_now - datetime.timedelta(hours=24)
+ difference = now - date
+ seconds = difference.seconds
+ days = difference.days
+
+ _ = self.translate
+ format = None
+ if not full_format:
+ if relative and days == 0:
+ if seconds < 50:
+ return _("1 second ago", "%(seconds)d seconds ago",
+ seconds) % { "seconds": seconds }
+
+ if seconds < 50 * 60:
+ minutes = round(seconds / 60.0)
+ return _("1 minute ago", "%(minutes)d minutes ago",
+ minutes) % { "minutes": minutes }
+
+ hours = round(seconds / (60.0 * 60))
+ return _("1 hour ago", "%(hours)d hours ago",
+ hours) % { "hours": hours }
+
+ if days == 0:
+ format = _("%(time)s")
+ elif days == 1 and local_date.day == local_yesterday.day and \
+ relative:
+ format = _("yesterday") if shorter else \
+ _("yesterday at %(time)s")
+ elif days < 5:
+ format = _("%(weekday)s") if shorter else \
+ _("%(weekday)s at %(time)s")
+ elif days < 334: # 11mo, since confusing for same month last year
+ format = _("%(month_name)s %(day)s") if shorter else \
+ _("%(month_name)s %(day)s at %(time)s")
+
+ if format is None:
+ format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
+ _("%(month_name)s %(day)s, %(year)s at %(time)s")
+
+ tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
+ if tfhour_clock:
+ str_time = "%d:%02d" % (local_date.hour, local_date.minute)
+ elif self.code == "zh_CN":
+ str_time = "%s%d:%02d" % (
+ (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
+ local_date.hour % 12 or 12, local_date.minute)
+ else:
+ str_time = "%d:%02d %s" % (
+ local_date.hour % 12 or 12, local_date.minute,
+ ("am", "pm")[local_date.hour >= 12])
+
+ return format % {
+ "month_name": self._months[local_date.month - 1],
+ "weekday": self._weekdays[local_date.weekday()],
+ "day": str(local_date.day),
+ "year": str(local_date.year),
+ "time": str_time
+ }
+
+ def format_day(self, date, gmt_offset=0, dow=True):
+ """Formats the given date as a day of week.
+
+ Example: "Monday, January 22". You can remove the day of week with
+ dow=False.
+ """
+ local_date = date - datetime.timedelta(minutes=gmt_offset)
+ _ = self.translate
+ if dow:
+ return _("%(weekday)s, %(month_name)s %(day)s") % {
+ "month_name": self._months[local_date.month - 1],
+ "weekday": self._weekdays[local_date.weekday()],
+ "day": str(local_date.day),
+ }
+ else:
+ return _("%(month_name)s %(day)s") % {
+ "month_name": self._months[local_date.month - 1],
+ "day": str(local_date.day),
+ }
+
+ def list(self, parts):
+ """Returns a comma-separated list for the given list of parts.
+
+ The format is, e.g., "A, B and C", "A and B" or just "A" for lists
+ of size 1.
+ """
+ _ = self.translate
+ if len(parts) == 0: return ""
+ if len(parts) == 1: return parts[0]
+ comma = u' \u0648 ' if self.code.startswith("fa") else u", "
+ return _("%(commas)s and %(last)s") % {
+ "commas": comma.join(parts[:-1]),
+ "last": parts[len(parts) - 1],
+ }
+
+ def friendly_number(self, value):
+ """Returns a comma-separated number for the given integer."""
+ if self.code not in ("en", "en_US"):
+ return str(value)
+ value = str(value)
+ parts = []
+ while value:
+ parts.append(value[-3:])
+ value = value[:-3]
+ return ",".join(reversed(parts))
+
+class CSVLocale(Locale):
+ """Locale implementation using tornado's CSV translation format."""
+ def translate(self, message, plural_message=None, count=None):
+ if plural_message is not None:
+ assert count is not None
+ if count != 1:
+ message = plural_message
+ message_dict = self.translations.get("plural", {})
+ else:
+ message_dict = self.translations.get("singular", {})
+ else:
+ message_dict = self.translations.get("unknown", {})
+ return message_dict.get(message, message)
+
+class GettextLocale(Locale):
+ """Locale implementation using the gettext module."""
+ def translate(self, message, plural_message=None, count=None):
+ if plural_message is not None:
+ assert count is not None
+ return self.translations.ungettext(message, plural_message, count)
+ else:
+ return self.translations.ugettext(message)
+
+LOCALE_NAMES = {
+ "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
+ "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"},
+ "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"},
+ "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
+ "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
+ "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
+ "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
+ "da_DK": {"name_en": u"Danish", "name": u"Dansk"},
+ "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"},
+ "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
+ "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_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
+ "et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
+ "eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
+ "fa_IR": {"name_en": u"Persian", "name": u"\u0641\u0627\u0631\u0633\u06cc"},
+ "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
+ "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
+ "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
+ "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
+ "gl_ES": {"name_en": u"Galician", "name": u"Galego"},
+ "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"},
+ "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
+ "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
+ "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
+ "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
+ "it_IT": {"name_en": u"Italian", "name": u"Italiano"},
+ "ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"},
+ "ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"},
+ "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
+ "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"},
+ "ml_IN": {"name_en": u"Malayalam", "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
+ "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
+ "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
+ "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
+ "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"},
+ "pl_PL": {"name_en": u"Polish", "name": u"Polski"},
+ "pt_BR": {"name_en": u"Portuguese (Brazil)", "name": u"Portugu\xeas (Brasil)"},
+ "pt_PT": {"name_en": u"Portuguese (Portugal)", "name": u"Portugu\xeas (Portugal)"},
+ "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"},
+ "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
+ "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
+ "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
+ "sr_RS": {"name_en": u"Serbian", "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
+ "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
+ "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
+ "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"},
+ "th_TH": {"name_en": u"Thai", "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
+ "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
+ "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"},
+ "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_TW": {"name_en": u"Chinese (Traditional)", "name": u"\u4e2d\u6587(\u7e41\u9ad4)"},
+}
diff --git a/tornado/netutil.py b/tornado/netutil.py
new file mode 100644
index 0000000..1b51e4e
--- /dev/null
+++ b/tornado/netutil.py
@@ -0,0 +1,319 @@
+#
+# 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.
+
+"""Miscellaneous network utility code."""
+
+import errno
+import logging
+import os
+import socket
+import stat
+
+from tornado import process
+from tornado.ioloop import IOLoop
+from tornado.iostream import IOStream, SSLIOStream
+from tornado.platform.auto import set_close_exec
+
+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
+
+ 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:
+ logging.error("Error in connection callback", exc_info=True)
+
+
+def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128):
+ """Creates listening sockets bound to the given port and address.
+
+ Returns a list of socket objects (multiple sockets are returned if
+ the given address maps to multiple IP addresses, which is most common
+ for mixed IPv4 and IPv6 use).
+
+ 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()``.
+ """
+ sockets = []
+ if address == "":
+ address = None
+ flags = socket.AI_PASSIVE
+ if hasattr(socket, "AI_ADDRCONFIG"):
+ # AI_ADDRCONFIG ensures that we only try to bind on ipv6
+ # if the system is configured for it, but the flag doesn't
+ # exist on some platforms (specifically WinXP, although
+ # newer versions of windows have it)
+ flags |= socket.AI_ADDRCONFIG
+ for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM,
+ 0, flags)):
+ af, socktype, proto, canonname, sockaddr = res
+ sock = socket.socket(af, socktype, proto)
+ set_close_exec(sock.fileno())
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if af == socket.AF_INET6:
+ # On linux, ipv6 sockets accept ipv4 too by default,
+ # but this makes it impossible to bind to both
+ # 0.0.0.0 in ipv4 and :: in ipv6. On other systems,
+ # separate sockets *must* be used to listen for both ipv4
+ # and ipv6. For consistency, always disable ipv4 on our
+ # ipv6 sockets and use a separate ipv4 socket when needed.
+ #
+ # Python 2.x on windows doesn't have IPPROTO_IPV6.
+ if hasattr(socket, "IPPROTO_IPV6"):
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
+ sock.setblocking(0)
+ sock.bind(sockaddr)
+ sock.listen(backlog)
+ sockets.append(sock)
+ return sockets
+
+if hasattr(socket, 'AF_UNIX'):
+ def bind_unix_socket(file, mode=0600, backlog=128):
+ """Creates a listening unix socket.
+
+ If a socket with the given name already exists, it will be deleted.
+ If any other file with that name exists, an exception will be
+ raised.
+
+ Returns a socket object (not a list of socket objects like
+ `bind_sockets`)
+ """
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ set_close_exec(sock.fileno())
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.setblocking(0)
+ try:
+ st = os.stat(file)
+ except OSError, err:
+ if err.errno != errno.ENOENT:
+ raise
+ else:
+ if stat.S_ISSOCK(st.st_mode):
+ os.remove(file)
+ else:
+ raise ValueError("File %s exists and is not a socket", file)
+ sock.bind(file)
+ os.chmod(file, mode)
+ sock.listen(backlog)
+ return sock
+
+def add_accept_handler(sock, callback, io_loop=None):
+ """Adds an ``IOLoop`` event handler to accept new connections on ``sock``.
+
+ When a connection is accepted, ``callback(connection, address)`` will
+ be run (``connection`` is a socket object, and ``address`` is the
+ address of the other end of the connection). Note that this signature
+ is different from the ``callback(fd, events)`` signature used for
+ ``IOLoop`` handlers.
+ """
+ if io_loop is None:
+ io_loop = IOLoop.instance()
+ def accept_handler(fd, events):
+ while True:
+ try:
+ connection, address = sock.accept()
+ except socket.error, e:
+ if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
+ return
+ raise
+ callback(connection, address)
+ io_loop.add_handler(sock.fileno(), accept_handler, IOLoop.READ)
diff --git a/tornado/platform/__init__.py b/tornado/platform/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tornado/platform/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tornado/platform/auto.py b/tornado/platform/auto.py
new file mode 100644
index 0000000..b53aae5
--- /dev/null
+++ b/tornado/platform/auto.py
@@ -0,0 +1,30 @@
+#
+# 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.
+
+"""Implementation of platform-specific functionality.
+
+For each function or class described in `tornado.platform.interface`,
+the appropriate platform-specific implementation exists in this module.
+Most code that needs access to this functionality should do e.g.::
+
+ from tornado.platform.auto import set_close_exec
+"""
+
+import os
+
+if os.name == 'nt':
+ from tornado.platform.windows import set_close_exec, Waker
+else:
+ from tornado.platform.posix import set_close_exec, Waker
diff --git a/tornado/platform/interface.py b/tornado/platform/interface.py
new file mode 100644
index 0000000..557fa68
--- /dev/null
+++ b/tornado/platform/interface.py
@@ -0,0 +1,56 @@
+#
+# 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.
+
+"""Interfaces for platform-specific functionality.
+
+This module exists primarily for documentation purposes and as base classes
+for other tornado.platform modules. Most code should import the appropriate
+implementation from `tornado.platform.auto`.
+"""
+
+def set_close_exec(fd):
+ """Sets the close-on-exec bit (``FD_CLOEXEC``)for a file descriptor."""
+ raise NotImplementedError()
+
+class Waker(object):
+ """A socket-like object that can wake another thread from ``select()``.
+
+ The `~tornado.ioloop.IOLoop` will add the Waker's `fileno()` to
+ its ``select`` (or ``epoll`` or ``kqueue``) calls. When another
+ thread wants to wake up the loop, it calls `wake`. Once it has woken
+ up, it will call `consume` to do any necessary per-wake cleanup. When
+ the ``IOLoop`` is closed, it closes its waker too.
+ """
+ def fileno(self):
+ """Returns a file descriptor for this waker.
+
+ Must be suitable for use with ``select()`` or equivalent on the
+ local platform.
+ """
+ raise NotImplementedError()
+
+ def wake(self):
+ """Triggers activity on the waker's file descriptor."""
+ raise NotImplementedError()
+
+ def consume(self):
+ """Called after the listen has woken up to do any necessary cleanup."""
+ raise NotImplementedError()
+
+ def close(self):
+ """Closes the waker's file descriptor(s)."""
+ raise NotImplementedError()
+
+
diff --git a/tornado/platform/posix.py b/tornado/platform/posix.py
new file mode 100644
index 0000000..b04ba4d
--- /dev/null
+++ b/tornado/platform/posix.py
@@ -0,0 +1,61 @@
+#
+# 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.
+
+"""Posix implementations of platform-specific functionality."""
+
+import fcntl
+import os
+
+from tornado.platform import interface
+from tornado.util import b
+
+def set_close_exec(fd):
+ flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
+
+def _set_nonblocking(fd):
+ flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+ fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+
+class Waker(interface.Waker):
+ def __init__(self):
+ r, w = os.pipe()
+ _set_nonblocking(r)
+ _set_nonblocking(w)
+ set_close_exec(r)
+ set_close_exec(w)
+ self.reader = os.fdopen(r, "rb", 0)
+ self.writer = os.fdopen(w, "wb", 0)
+
+ def fileno(self):
+ return self.reader.fileno()
+
+ def wake(self):
+ try:
+ self.writer.write(b("x"))
+ except IOError:
+ pass
+
+ def consume(self):
+ try:
+ while True:
+ result = self.reader.read()
+ if not result: break;
+ except IOError:
+ pass
+
+ def close(self):
+ self.reader.close()
+ self.writer.close()
diff --git a/tornado/process.py b/tornado/process.py
new file mode 100644
index 0000000..652e7ca
--- /dev/null
+++ b/tornado/process.py
@@ -0,0 +1,148 @@
+#
+# 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.
+
+"""Utilities for working with multiple processes."""
+
+import errno
+import logging
+import os
+import sys
+import time
+
+from binascii import hexlify
+
+from tornado import ioloop
+
+try:
+ import multiprocessing # Python 2.6+
+except ImportError:
+ multiprocessing = None
+
+def cpu_count():
+ """Returns the number of processors on this machine."""
+ if multiprocessing is not None:
+ try:
+ return multiprocessing.cpu_count()
+ except NotImplementedError:
+ pass
+ try:
+ return os.sysconf("SC_NPROCESSORS_CONF")
+ except ValueError:
+ pass
+ logging.error("Could not detect number of processors; assuming 1")
+ return 1
+
+def _reseed_random():
+ if 'random' not in sys.modules:
+ return
+ import random
+ # If os.urandom is available, this method does the same thing as
+ # random.seed (at least as of python 2.6). If os.urandom is not
+ # available, we mix in the pid in addition to a timestamp.
+ try:
+ seed = long(hexlify(os.urandom(16)), 16)
+ except NotImplementedError:
+ seed = int(time.time() * 1000) ^ os.getpid()
+ random.seed(seed)
+
+
+_task_id = None
+
+def fork_processes(num_processes, max_restarts=100):
+ """Starts multiple worker processes.
+
+ 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 > 0, 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 ``fork_processes``.
+
+ In each child process, ``fork_processes`` returns its *task id*, a
+ number between 0 and ``num_processes``. Processes that exit
+ abnormally (due to a signal or non-zero exit status) are restarted
+ with the same id (up to ``max_restarts`` times). In the parent
+ process, ``fork_processes`` returns None if all child processes
+ have exited normally, but will otherwise only exit by throwing an
+ exception.
+ """
+ global _task_id
+ assert _task_id is None
+ if num_processes is None or num_processes <= 0:
+ num_processes = cpu_count()
+ if ioloop.IOLoop.initialized():
+ raise RuntimeError("Cannot run in multiple processes: IOLoop instance "
+ "has already been initialized. You cannot call "
+ "IOLoop.instance() before calling start_processes()")
+ logging.info("Starting %d processes", num_processes)
+ children = {}
+ def start_child(i):
+ pid = os.fork()
+ if pid == 0:
+ # child process
+ _reseed_random()
+ global _task_id
+ _task_id = i
+ return i
+ else:
+ children[pid] = i
+ return None
+ for i in range(num_processes):
+ id = start_child(i)
+ if id is not None: return id
+ num_restarts = 0
+ while children:
+ try:
+ pid, status = os.wait()
+ except OSError, e:
+ if e.errno == errno.EINTR:
+ continue
+ raise
+ if pid not in children:
+ continue
+ id = children.pop(pid)
+ if os.WIFSIGNALED(status):
+ logging.warning("child %d (pid %d) killed by signal %d, restarting",
+ id, pid, os.WTERMSIG(status))
+ elif os.WEXITSTATUS(status) != 0:
+ logging.warning("child %d (pid %d) exited with status %d, restarting",
+ id, pid, os.WEXITSTATUS(status))
+ else:
+ logging.info("child %d (pid %d) exited normally", id, pid)
+ continue
+ num_restarts += 1
+ if num_restarts > max_restarts:
+ raise RuntimeError("Too many child restarts, giving up")
+ new_id = start_child(id)
+ if new_id is not None: return new_id
+ # All child processes exited cleanly, so exit the master process
+ # instead of just returning to right after the call to
+ # fork_processes (which will probably just start up another IOLoop
+ # unless the caller checks the return value).
+ sys.exit(0)
+
+def task_id():
+ """Returns the current task id, if any.
+
+ Returns None if this process was not created by `fork_processes`.
+ """
+ global _task_id
+ return _task_id
diff --git a/tornado/stack_context.py b/tornado/stack_context.py
new file mode 100644
index 0000000..6007b39
--- /dev/null
+++ b/tornado/stack_context.py
@@ -0,0 +1,243 @@
+#
+# Copyright 2010 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.
+
+'''StackContext allows applications to maintain threadlocal-like state
+that follows execution as it moves to other execution contexts.
+
+The motivating examples are to eliminate the need for explicit
+async_callback wrappers (as in tornado.web.RequestHandler), and to
+allow some additional context to be kept for logging.
+
+This is slightly magic, but it's an extension of the idea that an exception
+handler is a kind of stack-local state and when that stack is suspended
+and resumed in a new context that state needs to be preserved. StackContext
+shifts the burden of restoring that state from each call site (e.g.
+wrapping each AsyncHTTPClient callback in async_callback) to the mechanisms
+that transfer control from one context to another (e.g. AsyncHTTPClient
+itself, IOLoop, thread pools, etc).
+
+Example usage::
+
+ @contextlib.contextmanager
+ def die_on_error():
+ try:
+ yield
+ except Exception:
+ logging.error("exception in asynchronous operation",exc_info=True)
+ sys.exit(1)
+
+ with StackContext(die_on_error):
+ # Any exception thrown here *or in callback and its desendents*
+ # will cause the process to exit instead of spinning endlessly
+ # in the ioloop.
+ http_client.fetch(url, callback)
+ ioloop.start()
+
+Most applications shouln't have to work with `StackContext` directly.
+Here are a few rules of thumb for when it's necessary:
+
+* If you're writing an asynchronous library that doesn't rely on a
+ stack_context-aware library like `tornado.ioloop` or `tornado.iostream`
+ (for example, if you're writing a thread pool), use
+ `stack_context.wrap()` before any asynchronous operations to capture the
+ stack context from where the operation was started.
+
+* If you're writing an asynchronous library that has some shared
+ resources (such as a connection pool), create those shared resources
+ within a ``with stack_context.NullContext():`` block. This will prevent
+ ``StackContexts`` from leaking from one request to another.
+
+* If you want to write something like an exception handler that will
+ persist across asynchronous calls, create a new `StackContext` (or
+ `ExceptionStackContext`), and make your asynchronous calls in a ``with``
+ block that references your `StackContext`.
+'''
+
+from __future__ import with_statement
+
+import contextlib
+import functools
+import itertools
+import sys
+import threading
+
+class _State(threading.local):
+ def __init__(self):
+ self.contexts = ()
+_state = _State()
+
+class StackContext(object):
+ '''Establishes the given context as a StackContext that will be transferred.
+
+ Note that the parameter is a callable that returns a context
+ manager, not the context itself. That is, where for a
+ non-transferable context manager you would say::
+
+ with my_context():
+
+ StackContext takes the function itself rather than its result::
+
+ with StackContext(my_context):
+ '''
+ def __init__(self, context_factory):
+ self.context_factory = context_factory
+
+ # Note that some of this code is duplicated in ExceptionStackContext
+ # below. ExceptionStackContext is more common and doesn't need
+ # the full generality of this class.
+ def __enter__(self):
+ self.old_contexts = _state.contexts
+ # _state.contexts is a tuple of (class, arg) pairs
+ _state.contexts = (self.old_contexts +
+ ((StackContext, self.context_factory),))
+ try:
+ self.context = self.context_factory()
+ self.context.__enter__()
+ except Exception:
+ _state.contexts = self.old_contexts
+ raise
+
+ def __exit__(self, type, value, traceback):
+ try:
+ return self.context.__exit__(type, value, traceback)
+ finally:
+ _state.contexts = self.old_contexts
+
+class ExceptionStackContext(object):
+ '''Specialization of StackContext for exception handling.
+
+ The supplied exception_handler function will be called in the
+ event of an uncaught exception in this context. The semantics are
+ similar to a try/finally clause, and intended use cases are to log
+ an error, close a socket, or similar cleanup actions. The
+ exc_info triple (type, value, traceback) will be passed to the
+ exception_handler function.
+
+ If the exception handler returns true, the exception will be
+ consumed and will not be propagated to other exception handlers.
+ '''
+ def __init__(self, exception_handler):
+ self.exception_handler = exception_handler
+
+ def __enter__(self):
+ self.old_contexts = _state.contexts
+ _state.contexts = (self.old_contexts +
+ ((ExceptionStackContext, self.exception_handler),))
+
+ def __exit__(self, type, value, traceback):
+ try:
+ if type is not None:
+ return self.exception_handler(type, value, traceback)
+ finally:
+ _state.contexts = self.old_contexts
+
+class NullContext(object):
+ '''Resets the StackContext.
+
+ Useful when creating a shared resource on demand (e.g. an AsyncHTTPClient)
+ where the stack that caused the creating is not relevant to future
+ operations.
+ '''
+ def __enter__(self):
+ self.old_contexts = _state.contexts
+ _state.contexts = ()
+
+ def __exit__(self, type, value, traceback):
+ _state.contexts = self.old_contexts
+
+class _StackContextWrapper(functools.partial):
+ pass
+
+def wrap(fn):
+ '''Returns a callable object that will restore the current StackContext
+ when executed.
+
+ Use this whenever saving a callback to be executed later in a
+ different execution context (either in a different thread or
+ asynchronously in the same thread).
+ '''
+ if fn is None or fn.__class__ is _StackContextWrapper:
+ return fn
+ # functools.wraps doesn't appear to work on functools.partial objects
+ #@functools.wraps(fn)
+ def wrapped(callback, contexts, *args, **kwargs):
+ if contexts is _state.contexts or not contexts:
+ callback(*args, **kwargs)
+ return
+ if not _state.contexts:
+ new_contexts = [cls(arg) for (cls, arg) in contexts]
+ # If we're moving down the stack, _state.contexts is a prefix
+ # of contexts. For each element of contexts not in that prefix,
+ # create a new StackContext object.
+ # If we're moving up the stack (or to an entirely different stack),
+ # _state.contexts will have elements not in contexts. Use
+ # NullContext to clear the state and then recreate from contexts.
+ elif (len(_state.contexts) > len(contexts) or
+ any(a[1] is not b[1]
+ for a, b in itertools.izip(_state.contexts, contexts))):
+ # contexts have been removed or changed, so start over
+ new_contexts = ([NullContext()] +
+ [cls(arg) for (cls,arg) in contexts])
+ else:
+ new_contexts = [cls(arg)
+ for (cls, arg) in contexts[len(_state.contexts):]]
+ if len(new_contexts) > 1:
+ with _nested(*new_contexts):
+ callback(*args, **kwargs)
+ elif new_contexts:
+ with new_contexts[0]:
+ callback(*args, **kwargs)
+ else:
+ callback(*args, **kwargs)
+ if _state.contexts:
+ return _StackContextWrapper(wrapped, fn, _state.contexts)
+ else:
+ return _StackContextWrapper(fn)
+
+@contextlib.contextmanager
+def _nested(*managers):
+ """Support multiple context managers in a single with-statement.
+
+ Copied from the python 2.6 standard library. It's no longer present
+ in python 3 because the with statement natively supports multiple
+ context managers, but that doesn't help if the list of context
+ managers is not known until runtime.
+ """
+ exits = []
+ vars = []
+ exc = (None, None, None)
+ try:
+ for mgr in managers:
+ exit = mgr.__exit__
+ enter = mgr.__enter__
+ vars.append(enter())
+ exits.append(exit)
+ yield vars
+ except:
+ exc = sys.exc_info()
+ finally:
+ while exits:
+ exit = exits.pop()
+ try:
+ if exit(*exc):
+ exc = (None, None, None)
+ except:
+ exc = sys.exc_info()
+ if exc != (None, None, None):
+ # Don't rely on sys.exc_info() still containing
+ # the right information. Another exception may
+ # have been raised and caught by an exit method
+ raise exc[0], exc[1], exc[2]
+
diff --git a/tornado/template.py b/tornado/template.py
new file mode 100644
index 0000000..4f8a7c4
--- /dev/null
+++ b/tornado/template.py
@@ -0,0 +1,825 @@
+#
+# 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.
+
+"""A simple template system that compiles templates to Python code.
+
+Basic usage looks like::
+
+ t = template.Template("<html>{{ myvalue }}</html>")
+ print t.generate(myvalue="XXX")
+
+Loader is a class that loads templates from a root directory and caches
+the compiled templates::
+
+ loader = template.Loader("/home/btaylor")
+ print loader.load("test.html").generate(myvalue="XXX")
+
+We compile all templates to raw Python. Error-reporting is currently... uh,
+interesting. Syntax for the templates::
+
+ ### base.html
+ <html>
+ <head>
+ <title>{% block title %}Default title{% end %}</title>
+ </head>
+ <body>
+ <ul>
+ {% for student in students %}
+ {% block student %}
+ <li>{{ escape(student.name) }}</li>
+ {% end %}
+ {% end %}
+ </ul>
+ </body>
+ </html>
+
+ ### bold.html
+ {% extends "base.html" %}
+
+ {% block title %}A bolder title{% end %}
+
+ {% block student %}
+ <li><span style="bold">{{ escape(student.name) }}</span></li>
+ {% end %}
+
+Unlike most other template systems, we do not put any restrictions on the
+expressions you can include in your statements. if and for blocks get
+translated exactly into Python, you can do complex expressions like::
+
+ {% for student in [p for p in people if p.student and p.age > 23] %}
+ <li>{{ escape(student.name) }}</li>
+ {% end %}
+
+Translating directly to Python means you can apply functions to expressions
+easily, like the escape() function in the examples above. You can pass
+functions in to your template just like any other variable::
+
+ ### Python code
+ def add(x, y):
+ return x + y
+ template.execute(add=add)
+
+ ### The template
+ {{ add(1, 2) }}
+
+We provide the functions escape(), url_escape(), json_encode(), and squeeze()
+to all templates by default.
+
+Typical applications do not create `Template` or `Loader` instances by
+hand, but instead use the `render` and `render_string` methods of
+`tornado.web.RequestHandler`, which load templates automatically based
+on the ``template_path`` `Application` setting.
+
+Syntax Reference
+----------------
+
+Template expressions are surrounded by double curly braces: ``{{ ... }}``.
+The contents may be any python expression, which will be escaped according
+to the current autoescape setting and inserted into the output. Other
+template directives use ``{% %}``. These tags may be escaped as ``{{!``
+and ``{%!`` if you need to include a literal ``{{`` or ``{%`` in the output.
+
+To comment out a section so that it is omitted from the output, surround it
+with ``{# ... #}``.
+
+``{% apply *function* %}...{% end %}``
+ Applies a function to the output of all template code between ``apply``
+ and ``end``::
+
+ {% apply linkify %}{{name}} said: {{message}}{% end %}
+
+``{% autoescape *function* %}``
+ Sets the autoescape mode for the current file. This does not affect
+ other files, even those referenced by ``{% include %}``. Note that
+ autoescaping can also be configured globally, at the `Application`
+ or `Loader`.::
+
+ {% autoescape xhtml_escape %}
+ {% autoescape None %}
+
+``{% block *name* %}...{% end %}``
+ Indicates a named, replaceable block for use with ``{% extends %}``.
+ Blocks in the parent template will be replaced with the contents of
+ the same-named block in a child template.::
+
+ <!-- base.html -->
+ <title>{% block title %}Default title{% end %}</title>
+
+ <!-- mypage.html -->
+ {% extends "base.html" %}
+ {% block title %}My page title{% end %}
+
+``{% comment ... %}``
+ A comment which will be removed from the template output. Note that
+ there is no ``{% end %}`` tag; the comment goes from the word ``comment``
+ to the closing ``%}`` tag.
+
+``{% extends *filename* %}``
+ Inherit from another template. Templates that use ``extends`` should
+ contain one or more ``block`` tags to replace content from the parent
+ template. Anything in the child template not contained in a ``block``
+ tag will be ignored. For an example, see the ``{% block %}`` tag.
+
+``{% for *var* in *expr* %}...{% end %}``
+ Same as the python ``for`` statement.
+
+``{% from *x* import *y* %}``
+ Same as the python ``import`` statement.
+
+``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}``
+ Conditional statement - outputs the first section whose condition is
+ true. (The ``elif`` and ``else`` sections are optional)
+
+``{% import *module* %}``
+ Same as the python ``import`` statement.
+
+``{% include *filename* %}``
+ Includes another template file. The included file can see all the local
+ variables as if it were copied directly to the point of the ``include``
+ directive (the ``{% autoescape %}`` directive is an exception).
+ Alternately, ``{% module Template(filename, **kwargs) %}`` may be used
+ to include another template with an isolated namespace.
+
+``{% module *expr* %}``
+ Renders a `~tornado.web.UIModule`. The output of the ``UIModule`` is
+ not escaped::
+
+ {% module Template("foo.html", arg=42) %}
+
+``{% raw *expr* %}``
+ Outputs the result of the given expression without autoescaping.
+
+``{% set *x* = *y* %}``
+ Sets a local variable.
+
+``{% try %}...{% except %}...{% finally %}...{% end %}``
+ Same as the python ``try`` statement.
+
+``{% while *condition* %}... {% end %}``
+ Same as the python ``while`` statement.
+"""
+
+from __future__ import with_statement
+
+import cStringIO
+import datetime
+import linecache
+import logging
+import os.path
+import posixpath
+import re
+import threading
+
+from tornado import escape
+from tornado.util import bytes_type, ObjectDict
+
+_DEFAULT_AUTOESCAPE = "xhtml_escape"
+_UNSET = object()
+
+class Template(object):
+ """A compiled template.
+
+ We compile into Python from the given template_string. You can generate
+ the template from variables with generate().
+ """
+ def __init__(self, template_string, name="<string>", loader=None,
+ compress_whitespace=None, autoescape=_UNSET):
+ self.name = name
+ if compress_whitespace is None:
+ compress_whitespace = name.endswith(".html") or \
+ name.endswith(".js")
+ if autoescape is not _UNSET:
+ self.autoescape = autoescape
+ elif loader:
+ self.autoescape = loader.autoescape
+ else:
+ self.autoescape = _DEFAULT_AUTOESCAPE
+ self.namespace = loader.namespace if loader else {}
+ reader = _TemplateReader(name, escape.native_str(template_string))
+ self.file = _File(self, _parse(reader, self))
+ self.code = self._generate_python(loader, compress_whitespace)
+ self.loader = loader
+ try:
+ # Under python2.5, the fake filename used here must match
+ # the module name used in __name__ below.
+ self.compiled = compile(
+ escape.to_unicode(self.code),
+ "%s.generated.py" % self.name.replace('.','_'),
+ "exec")
+ except Exception:
+ formatted_code = _format_code(self.code).rstrip()
+ logging.error("%s code:\n%s", self.name, formatted_code)
+ raise
+
+ def generate(self, **kwargs):
+ """Generate this template with the given arguments."""
+ namespace = {
+ "escape": escape.xhtml_escape,
+ "xhtml_escape": escape.xhtml_escape,
+ "url_escape": escape.url_escape,
+ "json_encode": escape.json_encode,
+ "squeeze": escape.squeeze,
+ "linkify": escape.linkify,
+ "datetime": datetime,
+ "_utf8": escape.utf8, # for internal use
+ "_string_types": (unicode, bytes_type),
+ # __name__ and __loader__ allow the traceback mechanism to find
+ # the generated source code.
+ "__name__": self.name.replace('.', '_'),
+ "__loader__": ObjectDict(get_source=lambda name: self.code),
+ }
+ namespace.update(self.namespace)
+ namespace.update(kwargs)
+ exec self.compiled in namespace
+ execute = namespace["_execute"]
+ # Clear the traceback module's cache of source data now that
+ # we've generated a new template (mainly for this module's
+ # unittests, where different tests reuse the same name).
+ linecache.clearcache()
+ try:
+ return execute()
+ except Exception:
+ formatted_code = _format_code(self.code).rstrip()
+ logging.error("%s code:\n%s", self.name, formatted_code)
+ raise
+
+ def _generate_python(self, loader, compress_whitespace):
+ buffer = cStringIO.StringIO()
+ try:
+ # named_blocks maps from names to _NamedBlock objects
+ named_blocks = {}
+ ancestors = self._get_ancestors(loader)
+ ancestors.reverse()
+ for ancestor in ancestors:
+ ancestor.find_named_blocks(loader, named_blocks)
+ self.file.find_named_blocks(loader, named_blocks)
+ writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template,
+ compress_whitespace)
+ ancestors[0].generate(writer)
+ return buffer.getvalue()
+ finally:
+ buffer.close()
+
+ def _get_ancestors(self, loader):
+ ancestors = [self.file]
+ for chunk in self.file.body.chunks:
+ if isinstance(chunk, _ExtendsBlock):
+ if not loader:
+ raise ParseError("{% extends %} block found, but no "
+ "template loader")
+ template = loader.load(chunk.name, self.name)
+ ancestors.extend(template._get_ancestors(loader))
+ return ancestors
+
+
+class BaseLoader(object):
+ """Base class for template loaders."""
+ def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None):
+ """Creates a template loader.
+
+ root_directory may be the empty string if this loader does not
+ use the filesystem.
+
+ autoescape must be either None or a string naming a function
+ in the template namespace, such as "xhtml_escape".
+ """
+ self.autoescape = autoescape
+ self.namespace = namespace or {}
+ self.templates = {}
+ # self.lock protects self.templates. It's a reentrant lock
+ # because templates may load other templates via `include` or
+ # `extends`. Note that thanks to the GIL this code would be safe
+ # even without the lock, but could lead to wasted work as multiple
+ # threads tried to compile the same template simultaneously.
+ self.lock = threading.RLock()
+
+ def reset(self):
+ """Resets the cache of compiled templates."""
+ with self.lock:
+ self.templates = {}
+
+ def resolve_path(self, name, parent_path=None):
+ """Converts a possibly-relative path to absolute (used internally)."""
+ raise NotImplementedError()
+
+ def load(self, name, parent_path=None):
+ """Loads a template."""
+ name = self.resolve_path(name, parent_path=parent_path)
+ with self.lock:
+ if name not in self.templates:
+ self.templates[name] = self._create_template(name)
+ return self.templates[name]
+
+ def _create_template(self, name):
+ raise NotImplementedError()
+
+class Loader(BaseLoader):
+ """A template loader that loads from a single root directory.
+
+ You must use a template loader to use template constructs like
+ {% extends %} and {% include %}. Loader caches all templates after
+ they are loaded the first time.
+ """
+ def __init__(self, root_directory, **kwargs):
+ super(Loader, self).__init__(**kwargs)
+ self.root = os.path.abspath(root_directory)
+
+ def resolve_path(self, name, parent_path=None):
+ if parent_path and not parent_path.startswith("<") and \
+ not parent_path.startswith("/") and \
+ not name.startswith("/"):
+ current_path = os.path.join(self.root, parent_path)
+ file_dir = os.path.dirname(os.path.abspath(current_path))
+ relative_path = os.path.abspath(os.path.join(file_dir, name))
+ if relative_path.startswith(self.root):
+ name = relative_path[len(self.root) + 1:]
+ return name
+
+ def _create_template(self, name):
+ path = os.path.join(self.root, name)
+ f = open(path, "r")
+ template = Template(f.read(), name=name, loader=self)
+ f.close()
+ return template
+
+
+class DictLoader(BaseLoader):
+ """A template loader that loads from a dictionary."""
+ def __init__(self, dict, **kwargs):
+ super(DictLoader, self).__init__(**kwargs)
+ self.dict = dict
+
+ def resolve_path(self, name, parent_path=None):
+ if parent_path and not parent_path.startswith("<") and \
+ not parent_path.startswith("/") and \
+ not name.startswith("/"):
+ file_dir = posixpath.dirname(parent_path)
+ name = posixpath.normpath(posixpath.join(file_dir, name))
+ return name
+
+ def _create_template(self, name):
+ return Template(self.dict[name], name=name, loader=self)
+
+
+class _Node(object):
+ def each_child(self):
+ return ()
+
+ def generate(self, writer):
+ raise NotImplementedError()
+
+ def find_named_blocks(self, loader, named_blocks):
+ for child in self.each_child():
+ child.find_named_blocks(loader, named_blocks)
+
+
+class _File(_Node):
+ def __init__(self, template, body):
+ self.template = template
+ self.body = body
+ self.line = 0
+
+ def generate(self, writer):
+ writer.write_line("def _execute():", self.line)
+ with writer.indent():
+ writer.write_line("_buffer = []", self.line)
+ writer.write_line("_append = _buffer.append", self.line)
+ self.body.generate(writer)
+ writer.write_line("return _utf8('').join(_buffer)", self.line)
+
+ def each_child(self):
+ return (self.body,)
+
+
+
+class _ChunkList(_Node):
+ def __init__(self, chunks):
+ self.chunks = chunks
+
+ def generate(self, writer):
+ for chunk in self.chunks:
+ chunk.generate(writer)
+
+ def each_child(self):
+ return self.chunks
+
+
+class _NamedBlock(_Node):
+ def __init__(self, name, body, template, line):
+ self.name = name
+ self.body = body
+ self.template = template
+ self.line = line
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ block = writer.named_blocks[self.name]
+ with writer.include(block.template, self.line):
+ block.body.generate(writer)
+
+ def find_named_blocks(self, loader, named_blocks):
+ named_blocks[self.name] = self
+ _Node.find_named_blocks(self, loader, named_blocks)
+
+
+class _ExtendsBlock(_Node):
+ def __init__(self, name):
+ self.name = name
+
+
+class _IncludeBlock(_Node):
+ def __init__(self, name, reader, line):
+ self.name = name
+ self.template_name = reader.name
+ self.line = line
+
+ def find_named_blocks(self, loader, named_blocks):
+ included = loader.load(self.name, self.template_name)
+ included.file.find_named_blocks(loader, named_blocks)
+
+ def generate(self, writer):
+ included = writer.loader.load(self.name, self.template_name)
+ with writer.include(included, self.line):
+ included.file.body.generate(writer)
+
+
+class _ApplyBlock(_Node):
+ def __init__(self, method, line, body=None):
+ self.method = method
+ self.line = line
+ self.body = body
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ method_name = "apply%d" % writer.apply_counter
+ writer.apply_counter += 1
+ writer.write_line("def %s():" % method_name, self.line)
+ with writer.indent():
+ writer.write_line("_buffer = []", self.line)
+ writer.write_line("_append = _buffer.append", self.line)
+ self.body.generate(writer)
+ writer.write_line("return _utf8('').join(_buffer)", self.line)
+ writer.write_line("_append(%s(%s()))" % (
+ self.method, method_name), self.line)
+
+
+class _ControlBlock(_Node):
+ def __init__(self, statement, line, body=None):
+ self.statement = statement
+ self.line = line
+ self.body = body
+
+ def each_child(self):
+ return (self.body,)
+
+ def generate(self, writer):
+ writer.write_line("%s:" % self.statement, self.line)
+ with writer.indent():
+ self.body.generate(writer)
+
+
+class _IntermediateControlBlock(_Node):
+ def __init__(self, statement, line):
+ self.statement = statement
+ self.line = line
+
+ def generate(self, writer):
+ writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1)
+
+
+class _Statement(_Node):
+ def __init__(self, statement, line):
+ self.statement = statement
+ self.line = line
+
+ def generate(self, writer):
+ writer.write_line(self.statement, self.line)
+
+
+class _Expression(_Node):
+ def __init__(self, expression, line, raw=False):
+ self.expression = expression
+ self.line = line
+ self.raw = raw
+
+ def generate(self, writer):
+ writer.write_line("_tmp = %s" % self.expression, self.line)
+ writer.write_line("if isinstance(_tmp, _string_types):"
+ " _tmp = _utf8(_tmp)", self.line)
+ writer.write_line("else: _tmp = _utf8(str(_tmp))", self.line)
+ if not self.raw and writer.current_template.autoescape is not None:
+ # In python3 functions like xhtml_escape return unicode,
+ # so we have to convert to utf8 again.
+ writer.write_line("_tmp = _utf8(%s(_tmp))" %
+ writer.current_template.autoescape, self.line)
+ writer.write_line("_append(_tmp)", self.line)
+
+class _Module(_Expression):
+ def __init__(self, expression, line):
+ super(_Module, self).__init__("_modules." + expression, line,
+ raw=True)
+
+class _Text(_Node):
+ def __init__(self, value, line):
+ self.value = value
+ self.line = line
+
+ def generate(self, writer):
+ value = self.value
+
+ # Compress lots of white space to a single character. If the whitespace
+ # breaks a line, have it continue to break a line, but just with a
+ # single \n character
+ if writer.compress_whitespace and "<pre>" not in value:
+ value = re.sub(r"([\t ]+)", " ", value)
+ value = re.sub(r"(\s*\n\s*)", "\n", value)
+
+ if value:
+ writer.write_line('_append(%r)' % escape.utf8(value), self.line)
+
+
+class ParseError(Exception):
+ """Raised for template syntax errors."""
+ pass
+
+
+class _CodeWriter(object):
+ def __init__(self, file, named_blocks, loader, current_template,
+ compress_whitespace):
+ self.file = file
+ self.named_blocks = named_blocks
+ self.loader = loader
+ self.current_template = current_template
+ self.compress_whitespace = compress_whitespace
+ self.apply_counter = 0
+ self.include_stack = []
+ self._indent = 0
+
+ def indent_size(self):
+ return self._indent
+
+ def indent(self):
+ class Indenter(object):
+ def __enter__(_):
+ self._indent += 1
+ return self
+
+ def __exit__(_, *args):
+ assert self._indent > 0
+ self._indent -= 1
+
+ return Indenter()
+
+ def include(self, template, line):
+ self.include_stack.append((self.current_template, line))
+ self.current_template = template
+
+ class IncludeTemplate(object):
+ def __enter__(_):
+ return self
+
+ def __exit__(_, *args):
+ self.current_template = self.include_stack.pop()[0]
+
+ return IncludeTemplate()
+
+ def write_line(self, line, line_number, indent=None):
+ if indent == None:
+ indent = self._indent
+ line_comment = ' # %s:%d' % (self.current_template.name, line_number)
+ if self.include_stack:
+ ancestors = ["%s:%d" % (tmpl.name, lineno)
+ for (tmpl, lineno) in self.include_stack]
+ line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
+ print >> self.file, " "*indent + line + line_comment
+
+
+class _TemplateReader(object):
+ def __init__(self, name, text):
+ self.name = name
+ self.text = text
+ self.line = 1
+ self.pos = 0
+
+ def find(self, needle, start=0, end=None):
+ assert start >= 0, start
+ pos = self.pos
+ start += pos
+ if end is None:
+ index = self.text.find(needle, start)
+ else:
+ end += pos
+ assert end >= start
+ index = self.text.find(needle, start, end)
+ if index != -1:
+ index -= pos
+ return index
+
+ def consume(self, count=None):
+ if count is None:
+ count = len(self.text) - self.pos
+ newpos = self.pos + count
+ self.line += self.text.count("\n", self.pos, newpos)
+ s = self.text[self.pos:newpos]
+ self.pos = newpos
+ return s
+
+ def remaining(self):
+ return len(self.text) - self.pos
+
+ def __len__(self):
+ return self.remaining()
+
+ def __getitem__(self, key):
+ if type(key) is slice:
+ size = len(self)
+ start, stop, step = key.indices(size)
+ if start is None: start = self.pos
+ else: start += self.pos
+ if stop is not None: stop += self.pos
+ return self.text[slice(start, stop, step)]
+ elif key < 0:
+ return self.text[key]
+ else:
+ return self.text[self.pos + key]
+
+ def __str__(self):
+ return self.text[self.pos:]
+
+
+def _format_code(code):
+ lines = code.splitlines()
+ format = "%%%dd %%s\n" % len(repr(len(lines) + 1))
+ return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
+
+
+def _parse(reader, template, in_block=None):
+ body = _ChunkList([])
+ while True:
+ # Find next template directive
+ curly = 0
+ while True:
+ curly = reader.find("{", curly)
+ if curly == -1 or curly + 1 == reader.remaining():
+ # EOF
+ if in_block:
+ raise ParseError("Missing {%% end %%} block for %s" %
+ in_block)
+ body.chunks.append(_Text(reader.consume(), reader.line))
+ return body
+ # If the first curly brace is not the start of a special token,
+ # start searching from the character after it
+ if reader[curly + 1] not in ("{", "%", "#"):
+ curly += 1
+ continue
+ # When there are more than 2 curlies in a row, use the
+ # innermost ones. This is useful when generating languages
+ # like latex where curlies are also meaningful
+ if (curly + 2 < reader.remaining() and
+ reader[curly + 1] == '{' and reader[curly + 2] == '{'):
+ curly += 1
+ continue
+ break
+
+ # Append any text before the special token
+ if curly > 0:
+ cons = reader.consume(curly)
+ body.chunks.append(_Text(cons, reader.line))
+
+ start_brace = reader.consume(2)
+ line = reader.line
+
+ # Template directives may be escaped as "{{!" or "{%!".
+ # In this case output the braces and consume the "!".
+ # This is especially useful in conjunction with jquery templates,
+ # which also use double braces.
+ if reader.remaining() and reader[0] == "!":
+ reader.consume(1)
+ body.chunks.append(_Text(start_brace, line))
+ continue
+
+ # Comment
+ if start_brace == "{#":
+ end = reader.find("#}")
+ if end == -1:
+ raise ParseError("Missing end expression #} on line %d" % line)
+ contents = reader.consume(end).strip()
+ reader.consume(2)
+ continue
+
+ # Expression
+ if start_brace == "{{":
+ end = reader.find("}}")
+ if end == -1:
+ raise ParseError("Missing end expression }} on line %d" % line)
+ contents = reader.consume(end).strip()
+ reader.consume(2)
+ if not contents:
+ raise ParseError("Empty expression on line %d" % line)
+ body.chunks.append(_Expression(contents, line))
+ continue
+
+ # Block
+ assert start_brace == "{%", start_brace
+ end = reader.find("%}")
+ if end == -1:
+ raise ParseError("Missing end block %%} on line %d" % line)
+ contents = reader.consume(end).strip()
+ reader.consume(2)
+ if not contents:
+ raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
+
+ operator, space, suffix = contents.partition(" ")
+ suffix = suffix.strip()
+
+ # Intermediate ("else", "elif", etc) blocks
+ intermediate_blocks = {
+ "else": set(["if", "for", "while"]),
+ "elif": set(["if"]),
+ "except": set(["try"]),
+ "finally": set(["try"]),
+ }
+ allowed_parents = intermediate_blocks.get(operator)
+ if allowed_parents is not None:
+ if not in_block:
+ raise ParseError("%s outside %s block" %
+ (operator, allowed_parents))
+ if in_block not in allowed_parents:
+ raise ParseError("%s block cannot be attached to %s block" % (operator, in_block))
+ body.chunks.append(_IntermediateControlBlock(contents, line))
+ continue
+
+ # End tag
+ elif operator == "end":
+ if not in_block:
+ raise ParseError("Extra {%% end %%} block on line %d" % line)
+ return body
+
+ elif operator in ("extends", "include", "set", "import", "from",
+ "comment", "autoescape", "raw", "module"):
+ if operator == "comment":
+ continue
+ if operator == "extends":
+ suffix = suffix.strip('"').strip("'")
+ if not suffix:
+ raise ParseError("extends missing file path on line %d" % line)
+ block = _ExtendsBlock(suffix)
+ elif operator in ("import", "from"):
+ if not suffix:
+ raise ParseError("import missing statement on line %d" % line)
+ block = _Statement(contents, line)
+ elif operator == "include":
+ suffix = suffix.strip('"').strip("'")
+ if not suffix:
+ raise ParseError("include missing file path on line %d" % line)
+ block = _IncludeBlock(suffix, reader, line)
+ elif operator == "set":
+ if not suffix:
+ raise ParseError("set missing statement on line %d" % line)
+ block = _Statement(suffix, line)
+ elif operator == "autoescape":
+ fn = suffix.strip()
+ if fn == "None": fn = None
+ template.autoescape = fn
+ continue
+ elif operator == "raw":
+ block = _Expression(suffix, line, raw=True)
+ elif operator == "module":
+ block = _Module(suffix, line)
+ body.chunks.append(block)
+ continue
+
+ elif operator in ("apply", "block", "try", "if", "for", "while"):
+ # parse inner body recursively
+ block_body = _parse(reader, template, operator)
+ if operator == "apply":
+ if not suffix:
+ raise ParseError("apply missing method name on line %d" % line)
+ block = _ApplyBlock(suffix, line, block_body)
+ elif operator == "block":
+ if not suffix:
+ raise ParseError("block missing name on line %d" % line)
+ block = _NamedBlock(suffix, block_body, template, line)
+ else:
+ block = _ControlBlock(contents, line, block_body)
+ body.chunks.append(block)
+ continue
+
+ else:
+ raise ParseError("unknown operator: %r" % operator)
diff --git a/tornado/util.py b/tornado/util.py
new file mode 100644
index 0000000..6752401
--- /dev/null
+++ b/tornado/util.py
@@ -0,0 +1,47 @@
+"""Miscellaneous utility functions."""
+
+class ObjectDict(dict):
+ """Makes a dictionary behave like an object."""
+ def __getattr__(self, name):
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ self[name] = value
+
+
+def import_object(name):
+ """Imports an object by name.
+
+ import_object('x.y.z') is equivalent to 'from x.y import z'.
+
+ >>> import tornado.escape
+ >>> import_object('tornado.escape') is tornado.escape
+ True
+ >>> import_object('tornado.escape.utf8') is tornado.escape.utf8
+ True
+ """
+ parts = name.split('.')
+ obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
+ return getattr(obj, parts[-1])
+
+# Fake byte literal support: In python 2.6+, you can say b"foo" to get
+# a byte literal (str in 2.x, bytes in 3.x). There's no way to do this
+# in a way that supports 2.5, though, so we need a function wrapper
+# to convert our string literals. b() should only be applied to literal
+# latin1 strings. Once we drop support for 2.5, we can remove this function
+# and just use byte literals.
+if str is unicode:
+ def b(s):
+ return s.encode('latin1')
+ bytes_type = bytes
+else:
+ def b(s):
+ return s
+ bytes_type = str
+
+def doctests():
+ import doctest
+ return doctest.DocTestSuite()
diff --git a/tornado/web.py b/tornado/web.py
new file mode 100644
index 0000000..9e8a1fe
--- /dev/null
+++ b/tornado/web.py
@@ -0,0 +1,1984 @@
+#
+# 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.
+
+"""
+The Tornado web framework looks a bit like web.py (http://webpy.org/) or
+Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
+but with additional tools and optimizations to take advantage of the
+Tornado non-blocking web server and tools.
+
+Here is the canonical "Hello, world" example app::
+
+ import tornado.ioloop
+ import tornado.web
+
+ class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ if __name__ == "__main__":
+ application = tornado.web.Application([
+ (r"/", MainHandler),
+ ])
+ application.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()
+
+See the Tornado walkthrough on http://tornadoweb.org for more details
+and a good getting started guide.
+
+Thread-safety notes
+-------------------
+
+In general, methods on RequestHandler and elsewhere in tornado are not
+thread-safe. In particular, methods such as write(), finish(), and
+flush() must only be called from the main thread. If you use multiple
+threads it is important to use IOLoop.add_callback to transfer control
+back to the main thread before finishing the request.
+"""
+
+from __future__ import 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 logging
+import mimetypes
+import os.path
+import re
+import stat
+import sys
+import threading
+import time
+import tornado
+import traceback
+import types
+import urllib
+import urlparse
+import uuid
+
+from tornado import escape
+from tornado import locale
+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
+
+try:
+ from io import BytesIO # python 3
+except ImportError:
+ from cStringIO import StringIO as BytesIO # python 2
+
+class RequestHandler(object):
+ """Subclass this class and define get() or post() to make a handler.
+
+ If you want to support more methods than the standard GET/HEAD/POST, you
+ should override the class variable SUPPORTED_METHODS in your
+ RequestHandler class.
+ """
+ SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PUT", "OPTIONS")
+
+ _template_loaders = {} # {path: template.BaseLoader}
+ _template_loader_lock = threading.Lock()
+
+ def __init__(self, application, request, **kwargs):
+ 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.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
+ # 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())
+ self.ui["modules"] = self.ui["_modules"]
+ self.clear()
+ # Check since connection is not available in WSGI
+ if hasattr(self.request, "connection"):
+ self.request.connection.stream.set_close_callback(
+ self.on_connection_close)
+ self.initialize(**kwargs)
+
+ def initialize(self):
+ """Hook for subclass initialization.
+
+ A dictionary passed as the third argument of a url spec will be
+ supplied as keyword arguments to initialize().
+
+ Example::
+
+ class ProfileHandler(RequestHandler):
+ def initialize(self, database):
+ self.database = database
+
+ def get(self, username):
+ ...
+
+ app = Application([
+ (r'/user/(.*)', ProfileHandler, dict(database=database)),
+ ])
+ """
+ pass
+
+ @property
+ def settings(self):
+ """An alias for `self.application.settings`."""
+ return self.application.settings
+
+ def head(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def get(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def post(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def delete(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def put(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def options(self, *args, **kwargs):
+ raise HTTPError(405)
+
+ def prepare(self):
+ """Called at the beginning of a request before `get`/`post`/etc.
+
+ Override this method to perform common initialization regardless
+ of the request method.
+ """
+ pass
+
+ def on_finish(self):
+ """Called after the end of a request.
+
+ Override this method to perform cleanup, logging, etc.
+ This method is a counterpart to `prepare`. ``on_finish`` may
+ not produce any output, as it is called after the response
+ has been sent to the client.
+ """
+ pass
+
+ def on_connection_close(self):
+ """Called in async handlers if the client closed the connection.
+
+ Override this to clean up resources associated with
+ long-lived connections. Note that this method is called only if
+ the connection was closed during asynchronous processing; if you
+ need to do cleanup after every request override `on_finish`
+ instead.
+
+ Proxies may keep a connection open for a time (perhaps
+ indefinitely) after the client has gone away, so this method
+ may not be called promptly after the end user closes their
+ connection.
+ """
+ pass
+
+ 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",
+ }
+ self._list_headers = []
+ 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
+
+ def set_default_headers(self):
+ """Override this to set HTTP headers at the beginning of the request.
+
+ For example, this is the place to set a custom ``Server`` header.
+ Note that setting such headers in the normal flow of request
+ processing may not do what you want, since headers may be reset
+ during error handling.
+ """
+ pass
+
+ def set_status(self, status_code):
+ """Sets the status code for our response."""
+ assert status_code in httplib.responses
+ self._status_code = status_code
+
+ def get_status(self):
+ """Returns the status code for our response."""
+ return self._status_code
+
+ def set_header(self, name, value):
+ """Sets the given response header name and value.
+
+ If a datetime is given, we automatically format it according to the
+ HTTP specification. If the value is not a string, we convert it to
+ a string. All header values are then encoded as UTF-8.
+ """
+ self._headers[name] = self._convert_header_value(value)
+
+ def add_header(self, name, value):
+ """Adds the given response header and value.
+
+ 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)))
+
+ def _convert_header_value(self, value):
+ if isinstance(value, bytes_type):
+ pass
+ elif isinstance(value, unicode):
+ value = value.encode('utf-8')
+ elif isinstance(value, (int, long)):
+ # 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)
+ 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):
+ raise ValueError("Unsafe header value %r", value)
+ return value
+
+
+ _ARG_DEFAULT = []
+ def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
+ """Returns the value of the argument with the given name.
+
+ If default is not provided, the argument is considered to be
+ required, and we throw an HTTP 400 exception if it is missing.
+
+ If the argument appears in the url more than once, we return the
+ last value.
+
+ The returned value is always unicode.
+ """
+ args = self.get_arguments(name, strip=strip)
+ if not args:
+ if default is self._ARG_DEFAULT:
+ raise HTTPError(400, "Missing argument %s" % name)
+ return default
+ return args[-1]
+
+ def get_arguments(self, name, strip=True):
+ """Returns a list of the arguments with the given name.
+
+ If the argument is not present, returns an empty list.
+
+ 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):
+ # 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)
+ if strip:
+ v = v.strip()
+ values.append(v)
+ return values
+
+ def decode_argument(self, value, name=None):
+ """Decodes an argument from the request.
+
+ The argument has been percent-decoded and is now a byte string.
+ By default, this method decodes the argument as utf-8 and returns
+ a unicode string, but this may be overridden in subclasses.
+
+ This method is used as a filter for both get_argument() and for
+ values extracted from the url and passed to get()/post()/etc.
+
+ The name of the argument is provided if known, but may be None
+ (e.g. for unnamed groups in the url regex).
+ """
+ return _unicode(value)
+
+ @property
+ def cookies(self):
+ return self.request.cookies
+
+ def get_cookie(self, name, default=None):
+ """Gets the value of the cookie with the given name, else default."""
+ if self.request.cookies is not None and name in self.request.cookies:
+ return self.request.cookies[name].value
+ return default
+
+ def set_cookie(self, name, value, domain=None, expires=None, path="/",
+ expires_days=None, **kwargs):
+ """Sets the given cookie name/value with the given options.
+
+ Additional keyword arguments are set on the Cookie.Morsel
+ directly.
+ See http://docs.python.org/library/cookie.html#morsel-objects
+ for available attributes.
+ """
+ # The cookie library only accepts type str, in both python 2 and 3
+ name = escape.native_str(name)
+ value = escape.native_str(value)
+ if re.search(r"[\x00-\x20]", name + value):
+ # Don't let us accidentally inject bad stuff
+ raise ValueError("Invalid cookie %r: %r" % (name, value))
+ if not hasattr(self, "_new_cookies"):
+ self._new_cookies = []
+ new_cookie = Cookie.SimpleCookie()
+ self._new_cookies.append(new_cookie)
+ new_cookie[name] = value
+ if domain:
+ new_cookie[name]["domain"] = domain
+ if expires_days is not None and not expires:
+ expires = datetime.datetime.utcnow() + datetime.timedelta(
+ days=expires_days)
+ if expires:
+ timestamp = calendar.timegm(expires.utctimetuple())
+ new_cookie[name]["expires"] = email.utils.formatdate(
+ timestamp, localtime=False, usegmt=True)
+ if path:
+ new_cookie[name]["path"] = path
+ for k, v in kwargs.iteritems():
+ if k == 'max_age': k = 'max-age'
+ new_cookie[name][k] = v
+
+ def clear_cookie(self, name, path="/", domain=None):
+ """Deletes the cookie with the given name."""
+ expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
+ self.set_cookie(name, value="", path=path, expires=expires,
+ domain=domain)
+
+ def clear_all_cookies(self):
+ """Deletes all the cookies the user sent with this request."""
+ for name in self.request.cookies.iterkeys():
+ self.clear_cookie(name)
+
+ def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
+ """Signs and timestamps a cookie so it cannot be forged.
+
+ You must specify the ``cookie_secret`` setting in your Application
+ to use this method. It should be a long, random sequence of bytes
+ to be used as the HMAC secret for the signature.
+
+ To read a cookie set with this method, use `get_secure_cookie()`.
+
+ Note that the ``expires_days`` parameter sets the lifetime of the
+ cookie in the browser, but is independent of the ``max_age_days``
+ parameter to `get_secure_cookie`.
+ """
+ self.set_cookie(name, self.create_signed_value(name, value),
+ expires_days=expires_days, **kwargs)
+
+ def create_signed_value(self, name, value):
+ """Signs and timestamps a string so it cannot be forged.
+
+ Normally used via set_secure_cookie, but provided as a separate
+ method for non-cookie uses. To decode a value not stored
+ as a cookie use the optional value argument to get_secure_cookie.
+ """
+ self.require_setting("cookie_secret", "secure cookies")
+ return create_signed_value(self.application.settings["cookie_secret"],
+ name, value)
+
+ def get_secure_cookie(self, name, value=None, max_age_days=31):
+ """Returns the given signed cookie if it validates, or None."""
+ self.require_setting("cookie_secret", "secure cookies")
+ if value is None: value = self.get_cookie(name)
+ return decode_signed_value(self.application.settings["cookie_secret"],
+ name, value, max_age_days=max_age_days)
+
+ def redirect(self, url, permanent=False, status=None):
+ """Sends a redirect to the given (optionally relative) URL.
+
+ If the ``status`` argument is specified, that value is used as the
+ HTTP status code; otherwise either 301 (permanent) or 302
+ (temporary) is chosen based on the ``permanent`` argument.
+ The default is 302 (temporary).
+ """
+ if self._headers_written:
+ raise Exception("Cannot redirect after headers have been written")
+ if status is None:
+ status = 301 if permanent else 302
+ else:
+ assert isinstance(status, int) and 300 <= status <= 399
+ self.set_status(status)
+ # Remove whitespace
+ url = re.sub(b(r"[\x00-\x20]+"), "", utf8(url))
+ self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
+ url))
+ self.finish()
+
+ def write(self, chunk):
+ """Writes the given chunk to the output buffer.
+
+ To write the output to the network, use the flush() method below.
+
+ If the given chunk is a dictionary, we write it as JSON and set
+ the Content-Type of the response to be application/json.
+ (if you want to send JSON as a different Content-Type, call
+ set_header *after* calling write()).
+
+ Note that lists are not converted to JSON because of a potential
+ cross-site security vulnerability. All JSON output should be
+ wrapped in a dictionary. More details at
+ http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
+ """
+ if self._finished:
+ raise RuntimeError("Cannot write() after finish(). May be caused "
+ "by using async operations without the "
+ "@asynchronous decorator.")
+ if isinstance(chunk, dict):
+ chunk = escape.json_encode(chunk)
+ self.set_header("Content-Type", "application/json; charset=UTF-8")
+ chunk = utf8(chunk)
+ self._write_buffer.append(chunk)
+
+ def render(self, template_name, **kwargs):
+ """Renders the template with the given arguments as the response."""
+ html = self.render_string(template_name, **kwargs)
+
+ # Insert the additional JS and CSS added by the modules on the page
+ js_embed = []
+ js_files = []
+ css_embed = []
+ css_files = []
+ html_heads = []
+ html_bodies = []
+ for module in getattr(self, "_active_modules", {}).itervalues():
+ 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)):
+ js_files.append(file_part)
+ else:
+ js_files.extend(file_part)
+ embed_part = module.embedded_css()
+ if embed_part: css_embed.append(utf8(embed_part))
+ file_part = module.css_files()
+ if file_part:
+ if isinstance(file_part, (unicode, bytes_type)):
+ css_files.append(file_part)
+ else:
+ css_files.extend(file_part)
+ head_part = module.html_head()
+ if head_part: html_heads.append(utf8(head_part))
+ body_part = module.html_body()
+ if body_part: html_bodies.append(utf8(body_part))
+ def is_absolute(path):
+ return any(path.startswith(x) for x in ["/", "http:", "https:"])
+ if js_files:
+ # Maintain order of JavaScript files given by modules
+ paths = []
+ unique_paths = set()
+ for path in js_files:
+ if not is_absolute(path):
+ path = self.static_url(path)
+ if path not in unique_paths:
+ paths.append(path)
+ unique_paths.add(path)
+ 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:]
+ 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:]
+ if css_files:
+ paths = []
+ unique_paths = set()
+ for path in css_files:
+ if not is_absolute(path):
+ path = self.static_url(path)
+ if path not in unique_paths:
+ paths.append(path)
+ unique_paths.add(path)
+ 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:]
+ 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:]
+ if html_heads:
+ 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:]
+ self.finish(html)
+
+ def render_string(self, template_name, **kwargs):
+ """Generate the given template with the given arguments.
+
+ We return the generated string. To generate and write a template
+ as a response, use render() above.
+ """
+ # If no template_path is specified, use the path of the calling file
+ template_path = self.get_template_path()
+ if not template_path:
+ frame = sys._getframe(0)
+ web_file = frame.f_code.co_filename
+ while frame.f_code.co_filename == web_file:
+ frame = frame.f_back
+ template_path = os.path.dirname(frame.f_code.co_filename)
+ with RequestHandler._template_loader_lock:
+ if template_path not in RequestHandler._template_loaders:
+ loader = self.create_template_loader(template_path)
+ RequestHandler._template_loaders[template_path] = loader
+ else:
+ loader = RequestHandler._template_loaders[template_path]
+ t = loader.load(template_name)
+ args = dict(
+ handler=self,
+ request=self.request,
+ current_user=self.current_user,
+ locale=self.locale,
+ _=self.locale.translate,
+ static_url=self.static_url,
+ xsrf_form_html=self.xsrf_form_html,
+ reverse_url=self.application.reverse_url
+ )
+ args.update(self.ui)
+ args.update(kwargs)
+ return t.generate(**args)
+
+ def create_template_loader(self, template_path):
+ settings = self.application.settings
+ if "template_loader" in settings:
+ return settings["template_loader"]
+ kwargs = {}
+ if "autoescape" in settings:
+ # autoescape=None means "no escaping", so we have to be sure
+ # to only pass this kwarg if the user asked for it.
+ kwargs["autoescape"] = settings["autoescape"]
+ return template.Loader(template_path, **kwargs)
+
+
+ def flush(self, include_footers=False, callback=None):
+ """Flushes the current output buffer to the network.
+
+ The ``callback`` argument, if given, can be used for flow control:
+ it will be run when all flushed data has been written to the socket.
+ Note that only one flush callback can be outstanding at a time;
+ if another flush occurs before the previous flush's callback
+ has been run, the previous callback will be discarded.
+ """
+ if self.application._wsgi:
+ raise Exception("WSGI applications do not support flush()")
+
+ chunk = b("").join(self._write_buffer)
+ self._write_buffer = []
+ if not self._headers_written:
+ self._headers_written = True
+ for transform in self._transforms:
+ self._headers, chunk = transform.transform_first_chunk(
+ self._headers, chunk, include_footers)
+ headers = self._generate_headers()
+ else:
+ for transform in self._transforms:
+ chunk = transform.transform_chunk(chunk, include_footers)
+ headers = b("")
+
+ # Ignore the chunk and only write the headers for HEAD requests
+ if self.request.method == "HEAD":
+ if headers: self.request.write(headers, callback=callback)
+ return
+
+ if headers or chunk:
+ self.request.write(headers + chunk, callback=callback)
+
+ def finish(self, chunk=None):
+ """Finishes this response, ending the HTTP request."""
+ if self._finished:
+ raise RuntimeError("finish() called twice. May be caused "
+ "by using async operations without the "
+ "@asynchronous decorator.")
+
+ if chunk is not None: self.write(chunk)
+
+ # Automatically support ETags and add the Content-Length header if
+ # we have not flushed any content yet.
+ if not self._headers_written:
+ if (self._status_code == 200 and
+ self.request.method in ("GET", "HEAD") and
+ "Etag" not in self._headers):
+ etag = self.compute_etag()
+ if etag is not None:
+ inm = self.request.headers.get("If-None-Match")
+ if inm and inm.find(etag) != -1:
+ self._write_buffer = []
+ self.set_status(304)
+ else:
+ self.set_header("Etag", etag)
+ if "Content-Length" not in self._headers:
+ content_length = sum(len(part) for part in self._write_buffer)
+ self.set_header("Content-Length", content_length)
+
+ if hasattr(self.request, "connection"):
+ # Now that the request is finished, clear the callback we
+ # set on the IOStream (which would otherwise prevent the
+ # garbage collection of the RequestHandler when there
+ # are keepalive connections)
+ self.request.connection.stream.set_close_callback(None)
+
+ if not self.application._wsgi:
+ self.flush(include_footers=True)
+ self.request.finish()
+ self._log()
+ self._finished = True
+ self.on_finish()
+
+ def send_error(self, status_code=500, **kwargs):
+ """Sends the given HTTP error code to the browser.
+
+ If `flush()` has already been called, it is not possible to send
+ an error, so this method will simply terminate the response.
+ If output has been written but not yet flushed, it will be discarded
+ and replaced with the error page.
+
+ Override `write_error()` to customize the error page that is returned.
+ Additional keyword arguments are passed through to `write_error`.
+ """
+ if self._headers_written:
+ logging.error("Cannot send error response after headers written")
+ if not self._finished:
+ self.finish()
+ return
+ self.clear()
+ self.set_status(status_code)
+ try:
+ self.write_error(status_code, **kwargs)
+ except Exception:
+ logging.error("Uncaught exception in write_error", exc_info=True)
+ if not self._finished:
+ self.finish()
+
+ def write_error(self, status_code, **kwargs):
+ """Override to implement custom error pages.
+
+ ``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``.
+
+ For historical reasons, if a method ``get_error_html`` exists,
+ it will be used instead of the default ``write_error`` implementation.
+ ``get_error_html`` returned a string instead of producing output
+ normally, and had different semantics for exception handling.
+ Users of ``get_error_html`` are encouraged to convert their code
+ to override ``write_error`` instead.
+ """
+ if hasattr(self, 'get_error_html'):
+ if 'exc_info' in kwargs:
+ exc_info = kwargs.pop('exc_info')
+ kwargs['exception'] = exc_info[1]
+ try:
+ # Put the traceback into sys.exc_info()
+ raise exc_info[0], exc_info[1], exc_info[2]
+ except Exception:
+ self.finish(self.get_error_html(status_code, **kwargs))
+ else:
+ self.finish(self.get_error_html(status_code, **kwargs))
+ return
+ if self.settings.get("debug") and "exc_info" in kwargs:
+ # in debug mode, try to send a traceback
+ self.set_header('Content-Type', 'text/plain')
+ for line in traceback.format_exception(*kwargs["exc_info"]):
+ self.write(line)
+ self.finish()
+ else:
+ self.finish("<html><title>%(code)d: %(message)s</title>"
+ "<body>%(code)d: %(message)s</body></html>" % {
+ "code": status_code,
+ "message": httplib.responses[status_code],
+ })
+
+ @property
+ def locale(self):
+ """The local for the current session.
+
+ Determined by either get_user_locale, which you can override to
+ set the locale based on, e.g., a user preference stored in a
+ database, or get_browser_locale, which uses the Accept-Language
+ header.
+ """
+ if not hasattr(self, "_locale"):
+ self._locale = self.get_user_locale()
+ if not self._locale:
+ self._locale = self.get_browser_locale()
+ assert self._locale
+ return self._locale
+
+ def get_user_locale(self):
+ """Override to determine the locale from the authenticated user.
+
+ If None is returned, we fall back to get_browser_locale().
+
+ This method should return a tornado.locale.Locale object,
+ most likely obtained via a call like tornado.locale.get("en")
+ """
+ return None
+
+ def get_browser_locale(self, default="en_US"):
+ """Determines the user's locale from Accept-Language header.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+ """
+ if "Accept-Language" in self.request.headers:
+ languages = self.request.headers["Accept-Language"].split(",")
+ locales = []
+ for language in languages:
+ parts = language.strip().split(";")
+ if len(parts) > 1 and parts[1].startswith("q="):
+ try:
+ score = float(parts[1][2:])
+ except (ValueError, TypeError):
+ score = 0.0
+ else:
+ score = 1.0
+ locales.append((parts[0], score))
+ if locales:
+ locales.sort(key=lambda (l, s): s, reverse=True)
+ codes = [l[0] for l in locales]
+ return locale.get(*codes)
+ return locale.get(default)
+
+ @property
+ def current_user(self):
+ """The authenticated user for this request.
+
+ Determined by either get_current_user, which you can override to
+ set the user based on, e.g., a cookie. If that method is not
+ overridden, this method always returns None.
+
+ We lazy-load the current user the first time this method is called
+ and cache the result after that.
+ """
+ if not hasattr(self, "_current_user"):
+ self._current_user = self.get_current_user()
+ return self._current_user
+
+ def get_current_user(self):
+ """Override to determine the current user from, e.g., a cookie."""
+ return None
+
+ def get_login_url(self):
+ """Override to customize the login URL based on the request.
+
+ By default, we use the 'login_url' application setting.
+ """
+ self.require_setting("login_url", "@tornado.web.authenticated")
+ return self.application.settings["login_url"]
+
+ def get_template_path(self):
+ """Override to customize template path for each handler.
+
+ By default, we use the 'template_path' application setting.
+ Return None to load templates relative to the calling file.
+ """
+ return self.application.settings.get("template_path")
+
+ @property
+ def xsrf_token(self):
+ """The XSRF-prevention token for the current user/session.
+
+ To prevent cross-site request forgery, we set an '_xsrf' cookie
+ and include the same '_xsrf' value as an argument with all POST
+ requests. If the two do not match, we reject the form submission
+ as a potential forgery.
+
+ See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+ """
+ if not hasattr(self, "_xsrf_token"):
+ token = self.get_cookie("_xsrf")
+ if not token:
+ token = binascii.b2a_hex(uuid.uuid4().bytes)
+ expires_days = 30 if self.current_user else None
+ self.set_cookie("_xsrf", token, expires_days=expires_days)
+ self._xsrf_token = token
+ return self._xsrf_token
+
+ def check_xsrf_cookie(self):
+ """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
+
+ To prevent cross-site request forgery, we set an '_xsrf'
+ cookie and include the same value as a non-cookie
+ field with all POST requests. If the two do not match, we
+ reject the form submission as a potential forgery.
+
+ The _xsrf value may be set as either a form field named _xsrf
+ or in a custom HTTP header named X-XSRFToken or X-CSRFToken
+ (the latter is accepted for compatibility with Django).
+
+ See http://en.wikipedia.org/wiki/Cross-site_request_forgery
+
+ Prior to release 1.1.1, this check was ignored if the HTTP header
+ "X-Requested-With: XMLHTTPRequest" was present. This exception
+ has been shown to be insecure and has been removed. For more
+ information please see
+ http://www.djangoproject.com/weblog/2011/feb/08/security/
+ http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails
+ """
+ token = (self.get_argument("_xsrf", None) or
+ self.request.headers.get("X-Xsrftoken") or
+ self.request.headers.get("X-Csrftoken"))
+ if not token:
+ raise HTTPError(403, "'_xsrf' argument missing from POST")
+ if self.xsrf_token != token:
+ raise HTTPError(403, "XSRF cookie does not match POST argument")
+
+ def xsrf_form_html(self):
+ """An HTML <input/> element to be included with all POST forms.
+
+ It defines the _xsrf input value, which we check on all POST
+ requests to prevent cross-site request forgery. If you have set
+ the 'xsrf_cookies' application setting, you must include this
+ HTML within all of your HTML forms.
+
+ See check_xsrf_cookie() above for more information.
+ """
+ return '<input type="hidden" name="_xsrf" value="' + \
+ escape.xhtml_escape(self.xsrf_token) + '"/>'
+
+ def static_url(self, path, include_host=None):
+ """Returns a static URL for the given relative static file path.
+
+ This method requires you set the 'static_path' setting in your
+ application (which specifies the root directory of your static
+ files).
+
+ We append ?v=<signature> to the returned URL, which makes our
+ static file handler set an infinite expiration header on the
+ returned content. The signature is based on the content of the
+ file.
+
+ By default this method returns URLs relative to the current
+ host, but if ``include_host`` is true the URL returned will be
+ absolute. If this handler has an ``include_host`` attribute,
+ that value will be used as the default for all `static_url`
+ calls that do not pass ``include_host`` as a keyword argument.
+ """
+ self.require_setting("static_path", "static_url")
+ static_handler_class = self.settings.get(
+ "static_handler_class", StaticFileHandler)
+
+ if include_host is None:
+ include_host = getattr(self, "include_host", False)
+
+ if include_host:
+ base = self.request.protocol + "://" + self.request.host
+ else:
+ base = ""
+ return base + static_handler_class.make_static_url(self.settings, path)
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Obsolete - catches exceptions from the wrapped function.
+
+ This function is unnecessary since Tornado 1.1.
+ """
+ if callback is None:
+ return None
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception, e:
+ if self._headers_written:
+ logging.error("Exception after headers written",
+ exc_info=True)
+ else:
+ self._handle_request_exception(e)
+ return wrapper
+
+ def require_setting(self, name, feature="this feature"):
+ """Raises an exception if the given app setting is not defined."""
+ if not self.application.settings.get(name):
+ raise Exception("You must define the '%s' setting in your "
+ "application to use %s" % (name, feature))
+
+ def reverse_url(self, name, *args):
+ """Alias for `Application.reverse_url`."""
+ return self.application.reverse_url(name, *args)
+
+ def compute_etag(self):
+ """Computes the etag header to be used for this request.
+
+ May be overridden to provide custom etag implementations,
+ or may return None to disable tornado's default etag support.
+ """
+ hasher = hashlib.sha1()
+ for part in self._write_buffer:
+ hasher.update(part)
+ return '"%s"' % hasher.hexdigest()
+
+ def _stack_context_handle_exception(self, type, value, traceback):
+ try:
+ # For historical reasons _handle_request_exception only takes
+ # the exception value instead of the full triple,
+ # so re-raise the exception to ensure that it's in
+ # sys.exc_info()
+ raise type, value, traceback
+ except Exception:
+ self._handle_request_exception(value)
+ return True
+
+ def _execute(self, transforms, *args, **kwargs):
+ """Executes this request with the given output transforms."""
+ self._transforms = transforms
+ try:
+ if self.request.method not in self.SUPPORTED_METHODS:
+ raise HTTPError(405)
+ # 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.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)
+ if self._auto_finish and not self._finished:
+ self.finish()
+ except Exception, e:
+ self._handle_request_exception(e)
+
+ def _generate_headers(self):
+ lines = [utf8(self.request.version + " " +
+ str(self._status_code) +
+ " " + httplib.responses[self._status_code])]
+ lines.extend([(utf8(n) + b(": ") + utf8(v)) for n, v in
+ itertools.chain(self._headers.iteritems(), self._list_headers)])
+ for cookie_dict in getattr(self, "_new_cookies", []):
+ for cookie in cookie_dict.values():
+ lines.append(utf8("Set-Cookie: " + cookie.OutputString(None)))
+ return b("\r\n").join(lines) + b("\r\n\r\n")
+
+ def _log(self):
+ """Logs the current request.
+
+ Sort of deprecated since this functionality was moved to the
+ Application, but left in place for the benefit of existing apps
+ that have overridden this method.
+ """
+ self.application.log_request(self)
+
+ def _request_summary(self):
+ return self.request.method + " " + self.request.uri + \
+ " (" + self.request.remote_ip + ")"
+
+ def _handle_request_exception(self, e):
+ if isinstance(e, HTTPError):
+ 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)
+ 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(),
+ self.request, exc_info=True)
+ self.send_error(500, exc_info=sys.exc_info())
+
+ def _ui_module(self, name, module):
+ def render(*args, **kwargs):
+ if not hasattr(self, "_active_modules"):
+ self._active_modules = {}
+ if name not in self._active_modules:
+ self._active_modules[name] = module(self)
+ rendered = self._active_modules[name].render(*args, **kwargs)
+ return rendered
+ return render
+
+ def _ui_method(self, method):
+ return lambda *args, **kwargs: method(self, *args, **kwargs)
+
+
+def asynchronous(method):
+ """Wrap request handler methods with this if they are asynchronous.
+
+ If this decorator is given, the response is not finished when the
+ method returns. It is up to the request handler to call self.finish()
+ to finish the HTTP request. Without this decorator, the request is
+ automatically finished when the get() or post() method returns. ::
+
+ class MyRequestHandler(web.RequestHandler):
+ @web.asynchronous
+ def get(self):
+ http = httpclient.AsyncHTTPClient()
+ http.fetch("http://friendfeed.com/", self._on_download)
+
+ def _on_download(self, response):
+ self.write("Downloaded!")
+ self.finish()
+
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if self.application._wsgi:
+ raise Exception("@asynchronous is not supported for WSGI apps")
+ self._auto_finish = False
+ with stack_context.ExceptionStackContext(
+ self._stack_context_handle_exception):
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+def removeslash(method):
+ """Use this decorator to remove trailing slashes from the request path.
+
+ For example, a request to ``'/foo/'`` would redirect to ``'/foo'`` with this
+ decorator. Your request handler mapping should use a regular expression
+ like ``r'/foo/*'`` in conjunction with using the decorator.
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if self.request.path.endswith("/"):
+ if self.request.method in ("GET", "HEAD"):
+ uri = self.request.path.rstrip("/")
+ if uri: # don't try to redirect '/' to ''
+ if self.request.query: uri += "?" + self.request.query
+ self.redirect(uri)
+ return
+ else:
+ raise HTTPError(404)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+def addslash(method):
+ """Use this decorator to add a missing trailing slash to the request path.
+
+ For example, a request to '/foo' would redirect to '/foo/' with this
+ decorator. Your request handler mapping should use a regular expression
+ like r'/foo/?' in conjunction with using the decorator.
+ """
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if not self.request.path.endswith("/"):
+ if self.request.method in ("GET", "HEAD"):
+ uri = self.request.path + "/"
+ if self.request.query: uri += "?" + self.request.query
+ self.redirect(uri)
+ return
+ raise HTTPError(404)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+class Application(object):
+ """A collection of request handlers that make up a web application.
+
+ Instances of this class are callable and can be passed directly to
+ HTTPServer to serve the application::
+
+ application = web.Application([
+ (r"/", MainPageHandler),
+ ])
+ http_server = httpserver.HTTPServer(application)
+ http_server.listen(8080)
+ ioloop.IOLoop.instance().start()
+
+ The constructor for this class takes in a list of URLSpec objects
+ or (regexp, request_class) tuples. When we receive requests, we
+ iterate over the list in order and instantiate an instance of the
+ first request class whose regexp matches the request path.
+
+ Each tuple can contain an optional third element, which should be a
+ dictionary if it is present. That dictionary is passed as keyword
+ arguments to the contructor of the handler. This pattern is used
+ for the StaticFileHandler below (note that a StaticFileHandler
+ can be installed automatically with the static_path setting described
+ below)::
+
+ application = web.Application([
+ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+ ])
+
+ We support virtual hosts with the add_handlers method, which takes in
+ a host regular expression as the first argument::
+
+ application.add_handlers(r"www\.myhost\.com", [
+ (r"/article/([0-9]+)", ArticleHandler),
+ ])
+
+ You can serve static files by sending the static_path setting as a
+ keyword argument. We will serve those files from the /static/ URI
+ (this is configurable with the static_url_prefix setting),
+ 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
+
+ Additonal keyword arguments passed to the constructor are saved in the
+ `settings` dictionary, and are often referred to in documentation as
+ "application settings".
+ """
+ def __init__(self, handlers=None, default_host="", transforms=None,
+ wsgi=False, **settings):
+ if transforms is None:
+ self.transforms = []
+ if settings.get("gzip"):
+ self.transforms.append(GZipContentEncoding)
+ self.transforms.append(ChunkedTransferEncoding)
+ else:
+ self.transforms = transforms
+ self.handlers = []
+ self.named_handlers = {}
+ self.default_host = default_host
+ self.settings = settings
+ self.ui_modules = {'linkify': _linkify,
+ 'xsrf_form_html': _xsrf_form_html,
+ 'Template': TemplateModule,
+ }
+ self.ui_methods = {}
+ self._wsgi = wsgi
+ self._load_ui_modules(settings.get("ui_modules", {}))
+ self._load_ui_methods(settings.get("ui_methods", {}))
+ if self.settings.get("static_path"):
+ path = self.settings["static_path"]
+ handlers = list(handlers or [])
+ static_url_prefix = settings.get("static_url_prefix",
+ "/static/")
+ static_handler_class = settings.get("static_handler_class",
+ StaticFileHandler)
+ static_handler_args = settings.get("static_handler_args", {})
+ static_handler_args['path'] = path
+ for pattern in [re.escape(static_url_prefix) + r"(.*)",
+ r"/(favicon\.ico)", r"/(robots\.txt)"]:
+ handlers.insert(0, (pattern, static_handler_class,
+ static_handler_args))
+ if handlers: self.add_handlers(".*$", handlers)
+
+ # Automatically reload modified modules
+ if self.settings.get("debug") and not wsgi:
+ from tornado import autoreload
+ autoreload.start()
+
+ def listen(self, port, address="", **kwargs):
+ """Starts an HTTP server for this application on the given port.
+
+ This is a convenience alias for creating an HTTPServer object
+ and calling its listen method. Keyword arguments not
+ supported by HTTPServer.listen are passed to the HTTPServer
+ constructor. For advanced uses (e.g. preforking), do not use
+ this method; create an HTTPServer and call its bind/start
+ methods directly.
+
+ Note that after calling this method you still need to call
+ IOLoop.instance().start() to start the server.
+ """
+ # import is here rather than top level because HTTPServer
+ # is not importable on appengine
+ from tornado.httpserver import HTTPServer
+ server = HTTPServer(self, **kwargs)
+ server.listen(port, address)
+
+ def add_handlers(self, host_pattern, host_handlers):
+ """Appends the given handlers to our handler list.
+
+ Note that host patterns are processed sequentially in the
+ order they were added, and only the first matching pattern is
+ used. This means that all handlers for a given host must be
+ added in a single add_handlers call.
+ """
+ if not host_pattern.endswith("$"):
+ host_pattern += "$"
+ handlers = []
+ # The handlers with the wildcard host_pattern are a special
+ # case - they're added in the constructor but should have lower
+ # precedence than the more-precise handlers added later.
+ # If a wildcard handler group exists, it should always be last
+ # in the list, so insert new groups just before it.
+ if self.handlers and self.handlers[-1][0].pattern == '.*$':
+ self.handlers.insert(-1, (re.compile(host_pattern), handlers))
+ else:
+ self.handlers.append((re.compile(host_pattern), handlers))
+
+ for spec in host_handlers:
+ if type(spec) is type(()):
+ assert len(spec) in (2, 3)
+ pattern = spec[0]
+ handler = spec[1]
+
+ if isinstance(handler, str):
+ # import the Module and instantiate the class
+ # Must be a fully qualified name (module.ClassName)
+ handler = import_object(handler)
+
+ if len(spec) == 3:
+ kwargs = spec[2]
+ else:
+ kwargs = {}
+ spec = URLSpec(pattern, handler, kwargs)
+ handlers.append(spec)
+ if spec.name:
+ if spec.name in self.named_handlers:
+ logging.warning(
+ "Multiple handlers named %s; replacing previous value",
+ spec.name)
+ self.named_handlers[spec.name] = spec
+
+ def add_transform(self, transform_class):
+ """Adds the given OutputTransform to our transform list."""
+ self.transforms.append(transform_class)
+
+ def _get_host_handlers(self, request):
+ host = request.host.lower().split(':')[0]
+ for pattern, handlers in self.handlers:
+ if pattern.match(host):
+ return handlers
+ # Look for default host if not behind load balancer (for debugging)
+ if "X-Real-Ip" not in request.headers:
+ for pattern, handlers in self.handlers:
+ if pattern.match(self.default_host):
+ return handlers
+ return None
+
+ def _load_ui_methods(self, methods):
+ if type(methods) is 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():
+ if not name.startswith("_") and hasattr(fn, "__call__") \
+ and name[0].lower() == name[0]:
+ self.ui_methods[name] = fn
+
+ def _load_ui_modules(self, modules):
+ if type(modules) is types.ModuleType:
+ self._load_ui_modules(dict((n, getattr(modules, n))
+ for n in dir(modules)))
+ elif isinstance(modules, list):
+ for m in modules: self._load_ui_modules(m)
+ else:
+ assert isinstance(modules, dict)
+ for name, cls in modules.iteritems():
+ try:
+ if issubclass(cls, UIModule):
+ self.ui_modules[name] = cls
+ except TypeError:
+ pass
+
+ def __call__(self, request):
+ """Called by HTTPServer to execute the request."""
+ transforms = [t(request) for t in self.transforms]
+ handler = None
+ args = []
+ kwargs = {}
+ handlers = self._get_host_handlers(request)
+ if not handlers:
+ handler = RedirectHandler(
+ self, request, url="http://" + self.default_host + "/")
+ else:
+ for spec in handlers:
+ match = spec.regex.match(request.path)
+ if match:
+ handler = spec.handler_class(self, request, **spec.kwargs)
+ if spec.regex.groups:
+ # None-safe wrapper around url_unescape to handle
+ # unmatched optional groups correctly
+ def unquote(s):
+ if s is None: return s
+ return escape.url_unescape(s, encoding=None)
+ # Pass matched groups to the handler. Since
+ # match.groups() includes both named and unnamed groups,
+ # we want to use either groups or groupdict but not both.
+ # Note that args are passed as bytes so the handler can
+ # decide what encoding to use.
+
+ if spec.regex.groupindex:
+ kwargs = dict(
+ (k, unquote(v))
+ for (k, v) in match.groupdict().iteritems())
+ else:
+ args = [unquote(s) for s in match.groups()]
+ break
+ if not handler:
+ handler = ErrorHandler(self, request, status_code=404)
+
+ # In debug mode, re-compile templates and reload static files on every
+ # request so you don't need to restart to see changes
+ if self.settings.get("debug"):
+ with RequestHandler._template_loader_lock:
+ for loader in RequestHandler._template_loaders.values():
+ loader.reset()
+ StaticFileHandler.reset()
+
+ handler._execute(transforms, *args, **kwargs)
+ return handler
+
+ def reverse_url(self, name, *args):
+ """Returns a URL path for handler named `name`
+
+ The handler must be added to the application as a named URLSpec
+ """
+ if name in self.named_handlers:
+ return self.named_handlers[name].reverse(*args)
+ raise KeyError("%s not found in named urls" % name)
+
+ def log_request(self, handler):
+ """Writes a completed HTTP request to the logs.
+
+ By default writes to the python root logger. To change
+ this behavior either subclass Application and override this method,
+ or pass a function in the application settings dictionary as
+ 'log_function'.
+ """
+ if "log_function" in self.settings:
+ self.settings["log_function"](handler)
+ return
+ if handler.get_status() < 400:
+ log_method = logging.info
+ elif handler.get_status() < 500:
+ log_method = logging.warning
+ else:
+ log_method = logging.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):
+ self.status_code = status_code
+ self.log_message = log_message
+ self.args = args
+
+ def __str__(self):
+ message = "HTTP %d: %s" % (
+ self.status_code, httplib.responses[self.status_code])
+ if self.log_message:
+ return message + " (" + (self.log_message % self.args) + ")"
+ else:
+ return message
+
+
+class ErrorHandler(RequestHandler):
+ """Generates an error response with status_code for all requests."""
+ def initialize(self, status_code):
+ self.set_status(status_code)
+
+ def prepare(self):
+ raise HTTPError(self._status_code)
+
+
+class RedirectHandler(RequestHandler):
+ """Redirects the client to the given URL for all GET requests.
+
+ You should provide the keyword argument "url" to the handler, e.g.::
+
+ application = web.Application([
+ (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
+ ])
+ """
+ def initialize(self, url, permanent=True):
+ self._url = url
+ self._permanent = permanent
+
+ def get(self):
+ self.redirect(self._url, permanent=self._permanent)
+
+
+class StaticFileHandler(RequestHandler):
+ """A simple handler that can serve static content from a directory.
+
+ To map a path to this handler for a static data directory /var/www,
+ you would add a line to your application like::
+
+ application = web.Application([
+ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
+ ])
+
+ The local root directory of the content should be passed as the "path"
+ argument to the handler.
+
+ To support aggressive browser caching, if the argument "v" is given
+ with the path, we set an infinite HTTP expiration header. So, if you
+ want browsers to cache a file indefinitely, send them to, e.g.,
+ /static/images/myimage.png?v=xxx. Override ``get_cache_time`` method for
+ more fine-grained cache control.
+ """
+ CACHE_MAX_AGE = 86400*365*10 #10 years
+
+ _static_hashes = {}
+ _lock = threading.Lock() # protects _static_hashes
+
+ def initialize(self, path, default_filename=None):
+ self.root = os.path.abspath(path) + os.path.sep
+ self.default_filename = default_filename
+
+ @classmethod
+ def reset(cls):
+ with cls._lock:
+ cls._static_hashes = {}
+
+ def head(self, path):
+ self.get(path, include_body=False)
+
+ def get(self, path, include_body=True):
+ path = self.parse_url_path(path)
+ abspath = os.path.abspath(os.path.join(self.root, path))
+ # os.path.abspath strips a trailing /
+ # it needs to be temporarily added back for requests to root/
+ if not (abspath + os.path.sep).startswith(self.root):
+ raise HTTPError(403, "%s is not in root static directory", path)
+ if os.path.isdir(abspath) and self.default_filename is not None:
+ # need to look at the request.path here for when path is empty
+ # but there is some prefix to the path that was already
+ # trimmed by the routing
+ if not self.request.path.endswith("/"):
+ self.redirect(self.request.path + "/")
+ return
+ abspath = os.path.join(abspath, self.default_filename)
+ if not os.path.exists(abspath):
+ raise HTTPError(404)
+ if not os.path.isfile(abspath):
+ raise HTTPError(403, "%s is not a file", path)
+
+ stat_result = os.stat(abspath)
+ modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
+
+ self.set_header("Last-Modified", modified)
+
+ mime_type, encoding = mimetypes.guess_type(abspath)
+ if mime_type:
+ self.set_header("Content-Type", mime_type)
+
+ cache_time = self.get_cache_time(path, modified, mime_type)
+
+ if cache_time > 0:
+ 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)
+
+ # Check the If-Modified-Since, and don't send the result if the
+ # content has not been modified
+ ims_value = self.request.headers.get("If-Modified-Since")
+ if ims_value is not None:
+ date_tuple = email.utils.parsedate(ims_value)
+ if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
+ if if_since >= modified:
+ self.set_status(304)
+ return
+
+ 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:
+ assert self.request.method == "HEAD"
+ self.set_header("Content-Length", len(data))
+
+ def set_extra_headers(self, path):
+ """For subclass to add extra headers to the response"""
+ pass
+
+ def get_cache_time(self, path, modified, mime_type):
+ """Override to customize cache control behavior.
+
+ Return a positive number of seconds to trigger aggressive caching or 0
+ to mark resource as cacheable, only.
+
+ By default returns cache expiry of 10 years for resources requested
+ with "v" argument.
+ """
+ return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
+
+ @classmethod
+ def make_static_url(cls, settings, path):
+ """Constructs a versioned url for the given path.
+
+ This method may be overridden in subclasses (but note that it is
+ a class method rather than an instance method).
+
+ ``settings`` is the `Application.settings` dictionary. ``path``
+ is the static path being requested. The url returned should be
+ relative to the current host.
+ """
+ static_url_prefix = settings.get('static_url_prefix', '/static/')
+ version_hash = cls.get_version(settings, path)
+ if version_hash:
+ return static_url_prefix + path + "?v=" + version_hash
+ return static_url_prefix + path
+
+ @classmethod
+ def get_version(cls, settings, path):
+ """Generate the version string to be used in static URLs.
+
+ This method may be overridden in subclasses (but note that it
+ is a class method rather than a static method). The default
+ implementation uses a hash of the file's contents.
+
+ ``settings`` is the `Application.settings` dictionary and ``path``
+ is the relative location of the requested asset on the filesystem.
+ The returned value should be a string, or ``None`` if no version
+ could be determined.
+ """
+ abs_path = os.path.join(settings["static_path"], path)
+ with cls._lock:
+ hashes = cls._static_hashes
+ if abs_path not in hashes:
+ try:
+ f = open(abs_path, "rb")
+ hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
+ f.close()
+ except Exception:
+ logging.error("Could not open static file %r", path)
+ hashes[abs_path] = None
+ hsh = hashes.get(abs_path)
+ if hsh:
+ return hsh[:5]
+ return None
+
+ def parse_url_path(self, url_path):
+ """Converts a static URL path into a filesystem path.
+
+ ``url_path`` is the path component of the URL with
+ ``static_url_prefix`` removed. The return value should be
+ filesystem path relative to ``static_path``.
+ """
+ if os.path.sep != "/":
+ url_path = url_path.replace("/", os.path.sep)
+ return url_path
+
+
+class FallbackHandler(RequestHandler):
+ """A RequestHandler that wraps another HTTP server callback.
+
+ The fallback is a callable object that accepts an HTTPRequest,
+ such as an Application or tornado.wsgi.WSGIContainer. This is most
+ useful to use both tornado RequestHandlers and WSGI in the same server.
+ Typical usage::
+
+ wsgi_app = tornado.wsgi.WSGIContainer(
+ django.core.handlers.wsgi.WSGIHandler())
+ application = tornado.web.Application([
+ (r"/foo", FooHandler),
+ (r".*", FallbackHandler, dict(fallback=wsgi_app),
+ ])
+ """
+ def initialize(self, fallback):
+ self.fallback = fallback
+
+ def prepare(self):
+ self.fallback(self.request)
+ self._finished = True
+
+
+class OutputTransform(object):
+ """A transform modifies the result of an HTTP request (e.g., GZip encoding)
+
+ A new transform instance is created for every request. See the
+ ChunkedTransferEncoding example below if you want to implement a
+ new Transform.
+ """
+ def __init__(self, request):
+ pass
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ return headers, chunk
+
+ def transform_chunk(self, chunk, finishing):
+ return chunk
+
+
+class GZipContentEncoding(OutputTransform):
+ """Applies the gzip content encoding to the response.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
+ """
+ CONTENT_TYPES = set([
+ "text/plain", "text/html", "text/css", "text/xml", "application/javascript",
+ "application/x-javascript", "application/xml", "application/atom+xml",
+ "text/javascript", "application/json", "application/xhtml+xml"])
+ MIN_LENGTH = 5
+
+ def __init__(self, request):
+ self._gzipping = request.supports_http_1_1() and \
+ "gzip" in request.headers.get("Accept-Encoding", "")
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ if self._gzipping:
+ ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
+ self._gzipping = (ctype in self.CONTENT_TYPES) and \
+ (not finishing or len(chunk) >= self.MIN_LENGTH) and \
+ (finishing or "Content-Length" not in headers) and \
+ ("Content-Encoding" not in headers)
+ if self._gzipping:
+ headers["Content-Encoding"] = "gzip"
+ self._gzip_value = BytesIO()
+ self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
+ chunk = self.transform_chunk(chunk, finishing)
+ if "Content-Length" in headers:
+ headers["Content-Length"] = str(len(chunk))
+ return headers, chunk
+
+ def transform_chunk(self, chunk, finishing):
+ if self._gzipping:
+ self._gzip_file.write(chunk)
+ if finishing:
+ self._gzip_file.close()
+ else:
+ self._gzip_file.flush()
+ chunk = self._gzip_value.getvalue()
+ self._gzip_value.truncate(0)
+ self._gzip_value.seek(0)
+ return chunk
+
+
+class ChunkedTransferEncoding(OutputTransform):
+ """Applies the chunked transfer encoding to the response.
+
+ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
+ """
+ def __init__(self, request):
+ self._chunking = request.supports_http_1_1()
+
+ def transform_first_chunk(self, headers, chunk, finishing):
+ if self._chunking:
+ # No need to chunk the output if a Content-Length is specified
+ if "Content-Length" in headers or "Transfer-Encoding" in headers:
+ self._chunking = False
+ else:
+ headers["Transfer-Encoding"] = "chunked"
+ chunk = self.transform_chunk(chunk, finishing)
+ return headers, chunk
+
+ def transform_chunk(self, block, finishing):
+ if self._chunking:
+ # 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")
+ if finishing:
+ block += b("0\r\n\r\n")
+ return block
+
+
+def authenticated(method):
+ """Decorate methods with this to require that the user be logged in."""
+ @functools.wraps(method)
+ def wrapper(self, *args, **kwargs):
+ if not self.current_user:
+ if self.request.method in ("GET", "HEAD"):
+ url = self.get_login_url()
+ if "?" not in url:
+ if urlparse.urlsplit(url).scheme:
+ # if login url is absolute, make next absolute too
+ next_url = self.request.full_url()
+ else:
+ next_url = self.request.uri
+ url += "?" + urllib.urlencode(dict(next=next_url))
+ self.redirect(url)
+ return
+ raise HTTPError(403)
+ return method(self, *args, **kwargs)
+ return wrapper
+
+
+class UIModule(object):
+ """A UI re-usable, modular unit on a page.
+
+ UI modules often execute additional queries, and they can include
+ additional CSS and JavaScript that will be included in the output
+ page, which is automatically inserted on page render.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+ self.request = handler.request
+ self.ui = handler.ui
+ self.current_user = handler.current_user
+ self.locale = handler.locale
+
+ def render(self, *args, **kwargs):
+ """Overridden in subclasses to return this module's output."""
+ raise NotImplementedError()
+
+ def embedded_javascript(self):
+ """Returns a JavaScript string that will be embedded in the page."""
+ return None
+
+ def javascript_files(self):
+ """Returns a list of JavaScript files required by this module."""
+ return None
+
+ def embedded_css(self):
+ """Returns a CSS string that will be embedded in the page."""
+ return None
+
+ def css_files(self):
+ """Returns a list of CSS files required by this module."""
+ return None
+
+ def html_head(self):
+ """Returns a CSS string that will be put in the <head/> element"""
+ return None
+
+ def html_body(self):
+ """Returns an HTML string that will be put in the <body/> element"""
+ return None
+
+ def render_string(self, path, **kwargs):
+ """Renders a template and returns it as a string."""
+ return self.handler.render_string(path, **kwargs)
+
+class _linkify(UIModule):
+ def render(self, text, **kwargs):
+ return escape.linkify(text, **kwargs)
+
+class _xsrf_form_html(UIModule):
+ def render(self):
+ return self.handler.xsrf_form_html()
+
+class TemplateModule(UIModule):
+ """UIModule that simply renders the given template.
+
+ {% module Template("foo.html") %} is similar to {% include "foo.html" %},
+ but the module version gets its own namespace (with kwargs passed to
+ Template()) instead of inheriting the outer template's namespace.
+
+ Templates rendered through this module also get access to UIModule's
+ automatic javascript/css features. Simply call set_resources
+ inside the template and give it keyword arguments corresponding to
+ the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }}
+ Note that these resources are output once per template file, not once
+ per instantiation of the template, so they must not depend on
+ any arguments to the template.
+ """
+ def __init__(self, handler):
+ super(TemplateModule, self).__init__(handler)
+ # keep resources in both a list and a dict to preserve order
+ self._resource_list = []
+ self._resource_dict = {}
+
+ def render(self, path, **kwargs):
+ def set_resources(**kwargs):
+ if path not in self._resource_dict:
+ self._resource_list.append(kwargs)
+ self._resource_dict[path] = kwargs
+ else:
+ if self._resource_dict[path] != kwargs:
+ raise ValueError("set_resources called with different "
+ "resources for the same template")
+ return ""
+ return self.render_string(path, set_resources=set_resources,
+ **kwargs)
+
+ def _get_resources(self, key):
+ return (r[key] for r in self._resource_list if key in r)
+
+ def embedded_javascript(self):
+ return "\n".join(self._get_resources("embedded_javascript"))
+
+ def javascript_files(self):
+ result = []
+ for f in self._get_resources("javascript_files"):
+ if isinstance(f, (unicode, bytes_type)):
+ result.append(f)
+ else:
+ result.extend(f)
+ return result
+
+ def embedded_css(self):
+ return "\n".join(self._get_resources("embedded_css"))
+
+ def css_files(self):
+ result = []
+ for f in self._get_resources("css_files"):
+ if isinstance(f, (unicode, bytes_type)):
+ result.append(f)
+ else:
+ result.extend(f)
+ return result
+
+ def html_head(self):
+ return "".join(self._get_resources("html_head"))
+
+ def html_body(self):
+ return "".join(self._get_resources("html_body"))
+
+
+
+class URLSpec(object):
+ """Specifies mappings between URLs and handlers."""
+ def __init__(self, pattern, handler_class, kwargs={}, name=None):
+ """Creates a URLSpec.
+
+ Parameters:
+
+ pattern: Regular expression to be matched. Any groups in the regex
+ will be passed in to the handler's get/post/etc methods as
+ arguments.
+
+ handler_class: RequestHandler subclass to be invoked.
+
+ kwargs (optional): A dictionary of additional arguments to be passed
+ to the handler's constructor.
+
+ name (optional): A name for this handler. Used by
+ Application.reverse_url.
+ """
+ if not pattern.endswith('$'):
+ pattern += '$'
+ self.regex = re.compile(pattern)
+ assert len(self.regex.groupindex) in (0, self.regex.groups), \
+ ("groups in url regexes must either be all named or all "
+ "positional: %r" % self.regex.pattern)
+ self.handler_class = handler_class
+ self.kwargs = kwargs
+ self.name = name
+ self._path, self._group_count = self._find_groups()
+
+ def _find_groups(self):
+ """Returns a tuple (reverse string, group count) for a url.
+
+ For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
+ would return ('/%s/%s/', 2).
+ """
+ pattern = self.regex.pattern
+ if pattern.startswith('^'):
+ pattern = pattern[1:]
+ if pattern.endswith('$'):
+ pattern = pattern[:-1]
+
+ if self.regex.groups != pattern.count('('):
+ # The pattern is too complicated for our simplistic matching,
+ # so we can't support reversing it.
+ return (None, None)
+
+ pieces = []
+ for fragment in pattern.split('('):
+ if ')' in fragment:
+ paren_loc = fragment.index(')')
+ if paren_loc >= 0:
+ pieces.append('%s' + fragment[paren_loc + 1:])
+ else:
+ pieces.append(fragment)
+
+ return (''.join(pieces), self.regex.groups)
+
+ def reverse(self, *args):
+ assert self._path is not None, \
+ "Cannot reverse url regex " + self.regex.pattern
+ assert len(args) == self._group_count, "required number of arguments "\
+ "not found"
+ if not len(args):
+ return self._path
+ return self._path % tuple([str(a) for a in args])
+
+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
+
+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])
+ return value
+
+def decode_signed_value(secret, name, value, max_age_days=31):
+ if not value: return None
+ parts = utf8(value).split(b("|"))
+ if len(parts) != 3: 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)
+ return None
+ timestamp = int(parts[1])
+ if timestamp < time.time() - max_age_days * 86400:
+ logging.warning("Expired cookie %r", value)
+ return None
+ if timestamp > time.time() + 31 * 86400:
+ # _cookie_signature does not hash a delimiter between the
+ # parts of the cookie, so an attacker could transfer trailing
+ # 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)
+ return None
+ if parts[1].startswith(b("0")):
+ logging.warning("Tampered cookie %r", value)
+ try:
+ return base64.b64decode(parts[0])
+ except Exception:
+ return None
+
+def _create_signature(secret, *parts):
+ hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
+ for part in parts: hash.update(utf8(part))
+ return utf8(hash.hexdigest())
diff --git a/tornado/websocket.py b/tornado/websocket.py
new file mode 100644
index 0000000..8aa7777
--- /dev/null
+++ b/tornado/websocket.py
@@ -0,0 +1,650 @@
+"""Server-side implementation of the WebSocket protocol.
+
+`WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
+communication between the browser and server.
+
+.. warning::
+
+ The WebSocket protocol was recently finalized as `RFC 6455
+ <http://tools.ietf.org/html/rfc6455>`_ and is not yet supported in
+ all browsers. Refer to http://caniuse.com/websockets for details
+ on compatibility. In addition, during development the protocol
+ went through several incompatible versions, and some browsers only
+ support older versions. By default this module only supports the
+ latest version of the protocol, but optional support for an older
+ version (known as "draft 76" or "hixie-76") can be enabled by
+ overriding `WebSocketHandler.allow_draft76` (see that method's
+ documentation for caveats).
+"""
+# Author: Jacob Kristhammar, 2010
+
+import array
+import functools
+import hashlib
+import logging
+import struct
+import time
+import base64
+import tornado.escape
+import tornado.web
+
+from tornado.util import bytes_type, b
+
+class WebSocketHandler(tornado.web.RequestHandler):
+ """Subclass this class to create a basic WebSocket handler.
+
+ Override on_message to handle incoming messages. You can also override
+ open and on_close to handle opened and closed connections.
+
+ See http://dev.w3.org/html5/websockets/ for details on the
+ JavaScript interface. The protocol is specified at
+ http://tools.ietf.org/html/rfc6455.
+
+ Here is an example Web Socket handler that echos back all received messages
+ back to the client::
+
+ class EchoWebSocket(websocket.WebSocketHandler):
+ def open(self):
+ print "WebSocket opened"
+
+ def on_message(self, message):
+ self.write_message(u"You said: " + message)
+
+ def on_close(self):
+ print "WebSocket closed"
+
+ Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
+ but after the handshake, the protocol is message-based. Consequently,
+ most of the Tornado HTTP facilities are not available in handlers of this
+ type. The only communication methods available to you are write_message()
+ and close(). Likewise, your request handler class should
+ implement open() method rather than get() or post().
+
+ If you map the handler above to "/websocket" in your application, you can
+ invoke it in JavaScript with::
+
+ var ws = new WebSocket("ws://localhost:8888/websocket");
+ ws.onopen = function() {
+ ws.send("Hello, world");
+ };
+ ws.onmessage = function (evt) {
+ alert(evt.data);
+ };
+
+ This script pops up an alert box that says "You said: Hello, world".
+ """
+ def __init__(self, application, request, **kwargs):
+ tornado.web.RequestHandler.__init__(self, application, request,
+ **kwargs)
+ self.stream = request.connection.stream
+ self.ws_connection = None
+
+ def _execute(self, transforms, *args, **kwargs):
+ self.open_args = args
+ self.open_kwargs = kwargs
+
+ # Websocket only supports GET method
+ if self.request.method != 'GET':
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 405 Method Not Allowed\r\n\r\n"
+ ))
+ self.stream.close()
+ return
+
+ # Upgrade header should be present and should be equal to WebSocket
+ if self.request.headers.get("Upgrade", "").lower() != 'websocket':
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 400 Bad Request\r\n\r\n"
+ "Can \"Upgrade\" only to \"WebSocket\"."
+ ))
+ self.stream.close()
+ return
+
+ # Connection header should be upgrade. Some proxy servers/load balancers
+ # might mess with it.
+ headers = self.request.headers
+ connection = map(lambda s: s.strip().lower(), headers.get("Connection", "").split(","))
+ if 'upgrade' not in connection:
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 400 Bad Request\r\n\r\n"
+ "\"Connection\" must be \"Upgrade\"."
+ ))
+ self.stream.close()
+ return
+
+ # The difference between version 8 and 13 is that in 8 the
+ # client sends a "Sec-Websocket-Origin" header and in 13 it's
+ # simply "Origin".
+ if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
+ self.ws_connection = WebSocketProtocol13(self)
+ self.ws_connection.accept_connection()
+ elif (self.allow_draft76() and
+ "Sec-WebSocket-Version" not in self.request.headers):
+ self.ws_connection = WebSocketProtocol76(self)
+ self.ws_connection.accept_connection()
+ else:
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 426 Upgrade Required\r\n"
+ "Sec-WebSocket-Version: 8\r\n\r\n"))
+ self.stream.close()
+
+ def write_message(self, message, binary=False):
+ """Sends the given message to the client of this Web Socket.
+
+ The message may be either a string or a dict (which will be
+ encoded as json). If the ``binary`` argument is false, the
+ message will be sent as utf8; in binary mode any byte string
+ is allowed.
+ """
+ if isinstance(message, dict):
+ message = tornado.escape.json_encode(message)
+ self.ws_connection.write_message(message, binary=binary)
+
+ def select_subprotocol(self, subprotocols):
+ """Invoked when a new WebSocket requests specific subprotocols.
+
+ ``subprotocols`` is a list of strings identifying the
+ subprotocols proposed by the client. This method may be
+ overridden to return one of those strings to select it, or
+ ``None`` to not select a subprotocol. Failure to select a
+ subprotocol does not automatically abort the connection,
+ although clients may close the connection if none of their
+ proposed subprotocols was selected.
+ """
+ return None
+
+ def open(self):
+ """Invoked when a new WebSocket is opened.
+
+ The arguments to `open` are extracted from the `tornado.web.URLSpec`
+ regular expression, just like the arguments to
+ `tornado.web.RequestHandler.get`.
+ """
+ pass
+
+ def on_message(self, message):
+ """Handle incoming messages on the WebSocket
+
+ This method must be overridden.
+ """
+ raise NotImplementedError
+
+ def on_close(self):
+ """Invoked when the WebSocket is closed."""
+ pass
+
+ def close(self):
+ """Closes this Web Socket.
+
+ Once the close handshake is successful the socket will be closed.
+ """
+ self.ws_connection.close()
+
+ def allow_draft76(self):
+ """Override to enable support for the older "draft76" protocol.
+
+ The draft76 version of the websocket protocol is disabled by
+ default due to security concerns, but it can be enabled by
+ overriding this method to return True.
+
+ Connections using the draft76 protocol do not support the
+ ``binary=True`` flag to `write_message`.
+
+ Support for the draft76 protocol is deprecated and will be
+ removed in a future version of Tornado.
+ """
+ return False
+
+ def get_websocket_scheme(self):
+ """Return the url scheme used for this request, either "ws" or "wss".
+
+ This is normally decided by HTTPServer, but applications
+ may wish to override this if they are using an SSL proxy
+ that does not provide the X-Scheme header as understood
+ by HTTPServer.
+
+ Note that this is only used by the draft76 protocol.
+ """
+ return "wss" if self.request.protocol == "https" else "ws"
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions properly and closes this WebSocket if an exception
+ is uncaught. (Note that this is usually unnecessary thanks to
+ `tornado.stack_context`)
+ """
+ return self.ws_connection.async_callback(callback, *args, **kwargs)
+
+ def _not_supported(self, *args, **kwargs):
+ raise Exception("Method not supported for Web Sockets")
+
+ def on_connection_close(self):
+ if self.ws_connection:
+ self.ws_connection.on_connection_close()
+ self.ws_connection = None
+ self.on_close()
+
+
+for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
+ "set_status", "flush", "finish"]:
+ setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
+
+
+class WebSocketProtocol(object):
+ """Base class for WebSocket protocol versions.
+ """
+ def __init__(self, handler):
+ self.handler = handler
+ self.request = handler.request
+ self.stream = handler.stream
+ self.client_terminated = False
+ self.server_terminated = False
+
+ def async_callback(self, callback, *args, **kwargs):
+ """Wrap callbacks with this if they are used on asynchronous requests.
+
+ Catches exceptions properly and closes this WebSocket if an exception
+ is uncaught.
+ """
+ if args or kwargs:
+ callback = functools.partial(callback, *args, **kwargs)
+ def wrapper(*args, **kwargs):
+ try:
+ return callback(*args, **kwargs)
+ except Exception:
+ logging.error("Uncaught exception in %s",
+ self.request.path, exc_info=True)
+ self._abort()
+ return wrapper
+
+ def on_connection_close(self):
+ self._abort()
+
+ def _abort(self):
+ """Instantly aborts the WebSocket connection by closing the socket"""
+ self.client_terminated = True
+ self.server_terminated = True
+ self.stream.close() # forcibly tear down the connection
+ self.close() # let the subclass cleanup
+
+
+class WebSocketProtocol76(WebSocketProtocol):
+ """Implementation of the WebSockets protocol, version hixie-76.
+
+ This class provides basic functionality to process WebSockets requests as
+ specified in
+ http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+ """
+ def __init__(self, handler):
+ WebSocketProtocol.__init__(self, handler)
+ self.challenge = None
+ self._waiting = None
+
+ def accept_connection(self):
+ try:
+ self._handle_websocket_headers()
+ except ValueError:
+ logging.debug("Malformed WebSocket request received")
+ self._abort()
+ return
+
+ scheme = self.handler.get_websocket_scheme()
+
+ # draft76 only allows a single subprotocol
+ subprotocol_header = ''
+ subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
+ if subprotocol:
+ selected = self.handler.select_subprotocol([subprotocol])
+ if selected:
+ assert selected == subprotocol
+ subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+ # Write the initial headers before attempting to read the challenge.
+ # This is necessary when using proxies (such as HAProxy), which
+ # need to see the Upgrade headers before passing through the
+ # non-HTTP traffic that follows.
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
+ "Upgrade: WebSocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Server: TornadoServer/%(version)s\r\n"
+ "Sec-WebSocket-Origin: %(origin)s\r\n"
+ "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
+ "%(subprotocol)s"
+ "\r\n" % (dict(
+ version=tornado.version,
+ origin=self.request.headers["Origin"],
+ scheme=scheme,
+ host=self.request.host,
+ uri=self.request.uri,
+ subprotocol=subprotocol_header))))
+ self.stream.read_bytes(8, self._handle_challenge)
+
+ def challenge_response(self, challenge):
+ """Generates the challenge response that's needed in the handshake
+
+ The challenge parameter should be the raw bytes as sent from the
+ client.
+ """
+ key_1 = self.request.headers.get("Sec-Websocket-Key1")
+ key_2 = self.request.headers.get("Sec-Websocket-Key2")
+ try:
+ part_1 = self._calculate_part(key_1)
+ part_2 = self._calculate_part(key_2)
+ except ValueError:
+ raise ValueError("Invalid Keys/Challenge")
+ return self._generate_challenge_response(part_1, part_2, challenge)
+
+ def _handle_challenge(self, challenge):
+ try:
+ challenge_response = self.challenge_response(challenge)
+ except ValueError:
+ logging.debug("Malformed key data in WebSocket request")
+ self._abort()
+ return
+ self._write_response(challenge_response)
+
+ def _write_response(self, challenge):
+ self.stream.write(challenge)
+ self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+ self._receive_message()
+
+ def _handle_websocket_headers(self):
+ """Verifies all invariant- and required headers
+
+ If a header is missing or have an incorrect value ValueError will be
+ raised
+ """
+ fields = ("Origin", "Host", "Sec-Websocket-Key1",
+ "Sec-Websocket-Key2")
+ if not all(map(lambda f: self.request.headers.get(f), fields)):
+ raise ValueError("Missing/Invalid WebSocket headers")
+
+ def _calculate_part(self, key):
+ """Processes the key headers and calculates their key value.
+
+ Raises ValueError when feed invalid key."""
+ number = int(''.join(c for c in key if c.isdigit()))
+ spaces = len([c for c in key if c.isspace()])
+ try:
+ key_number = number // spaces
+ except (ValueError, ZeroDivisionError):
+ raise ValueError
+ return struct.pack(">I", key_number)
+
+ def _generate_challenge_response(self, part_1, part_2, part_3):
+ m = hashlib.md5()
+ m.update(part_1)
+ m.update(part_2)
+ m.update(part_3)
+ return m.digest()
+
+ def _receive_message(self):
+ self.stream.read_bytes(1, self._on_frame_type)
+
+ def _on_frame_type(self, byte):
+ frame_type = ord(byte)
+ if frame_type == 0x00:
+ self.stream.read_until(b("\xff"), self._on_end_delimiter)
+ elif frame_type == 0xff:
+ self.stream.read_bytes(1, self._on_length_indicator)
+ else:
+ self._abort()
+
+ def _on_end_delimiter(self, frame):
+ if not self.client_terminated:
+ self.async_callback(self.handler.on_message)(
+ frame[:-1].decode("utf-8", "replace"))
+ if not self.client_terminated:
+ self._receive_message()
+
+ def _on_length_indicator(self, byte):
+ if ord(byte) != 0x00:
+ self._abort()
+ return
+ self.client_terminated = True
+ self.close()
+
+ def write_message(self, message, binary=False):
+ """Sends the given message to the client of this Web Socket."""
+ if binary:
+ raise ValueError(
+ "Binary messages not supported by this version of websockets")
+ if isinstance(message, unicode):
+ message = message.encode("utf-8")
+ assert isinstance(message, bytes_type)
+ self.stream.write(b("\x00") + message + b("\xff"))
+
+ def close(self):
+ """Closes the WebSocket connection."""
+ if not self.server_terminated:
+ if not self.stream.closed():
+ self.stream.write("\xff\x00")
+ self.server_terminated = True
+ if self.client_terminated:
+ if self._waiting is not None:
+ self.stream.io_loop.remove_timeout(self._waiting)
+ self._waiting = None
+ self.stream.close()
+ elif self._waiting is None:
+ self._waiting = self.stream.io_loop.add_timeout(
+ time.time() + 5, self._abort)
+
+
+class WebSocketProtocol13(WebSocketProtocol):
+ """Implementation of the WebSocket protocol from RFC 6455.
+
+ This class supports versions 7 and 8 of the protocol in addition to the
+ final version 13.
+ """
+ def __init__(self, handler):
+ WebSocketProtocol.__init__(self, handler)
+ self._final_frame = False
+ self._frame_opcode = None
+ self._frame_mask = None
+ self._frame_length = None
+ self._fragmented_message_buffer = None
+ self._fragmented_message_opcode = None
+ self._waiting = None
+
+ def accept_connection(self):
+ try:
+ self._handle_websocket_headers()
+ self._accept_connection()
+ except ValueError:
+ logging.debug("Malformed WebSocket request received")
+ self._abort()
+ return
+
+ def _handle_websocket_headers(self):
+ """Verifies all invariant- and required headers
+
+ If a header is missing or have an incorrect value ValueError will be
+ raised
+ """
+ fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
+ if not all(map(lambda f: self.request.headers.get(f), fields)):
+ raise ValueError("Missing/Invalid WebSocket headers")
+
+ def _challenge_response(self):
+ sha1 = hashlib.sha1()
+ sha1.update(tornado.escape.utf8(
+ self.request.headers.get("Sec-Websocket-Key")))
+ sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
+ return tornado.escape.native_str(base64.b64encode(sha1.digest()))
+
+ def _accept_connection(self):
+ subprotocol_header = ''
+ subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '')
+ subprotocols = [s.strip() for s in subprotocols.split(',')]
+ if subprotocols:
+ selected = self.handler.select_subprotocol(subprotocols)
+ if selected:
+ assert selected in subprotocols
+ subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+
+ self.stream.write(tornado.escape.utf8(
+ "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n"
+ "%s"
+ "\r\n" % (self._challenge_response(), subprotocol_header)))
+
+ self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+ self._receive_frame()
+
+ def _write_frame(self, fin, opcode, data):
+ if fin:
+ finbit = 0x80
+ else:
+ finbit = 0
+ frame = struct.pack("B", finbit | opcode)
+ l = len(data)
+ if l < 126:
+ frame += struct.pack("B", l)
+ elif l <= 0xFFFF:
+ frame += struct.pack("!BH", 126, l)
+ else:
+ frame += struct.pack("!BQ", 127, l)
+ frame += data
+ self.stream.write(frame)
+
+ def write_message(self, message, binary=False):
+ """Sends the given message to the client of this Web Socket."""
+ if binary:
+ opcode = 0x2
+ else:
+ opcode = 0x1
+ message = tornado.escape.utf8(message)
+ assert isinstance(message, bytes_type)
+ self._write_frame(True, opcode, message)
+
+ def _receive_frame(self):
+ self.stream.read_bytes(2, self._on_frame_start)
+
+ def _on_frame_start(self, data):
+ header, payloadlen = struct.unpack("BB", data)
+ self._final_frame = header & 0x80
+ reserved_bits = header & 0x70
+ self._frame_opcode = header & 0xf
+ self._frame_opcode_is_control = self._frame_opcode & 0x8
+ if reserved_bits:
+ # client is using as-yet-undefined extensions; abort
+ self._abort()
+ return
+ if not (payloadlen & 0x80):
+ # Unmasked frame -> abort connection
+ self._abort()
+ return
+ payloadlen = payloadlen & 0x7f
+ if self._frame_opcode_is_control and payloadlen >= 126:
+ # control frames must have payload < 126
+ self._abort()
+ return
+ if payloadlen < 126:
+ self._frame_length = payloadlen
+ self.stream.read_bytes(4, self._on_masking_key)
+ elif payloadlen == 126:
+ self.stream.read_bytes(2, self._on_frame_length_16)
+ elif payloadlen == 127:
+ self.stream.read_bytes(8, self._on_frame_length_64)
+
+ def _on_frame_length_16(self, data):
+ self._frame_length = struct.unpack("!H", data)[0];
+ self.stream.read_bytes(4, self._on_masking_key);
+
+ def _on_frame_length_64(self, data):
+ self._frame_length = struct.unpack("!Q", data)[0];
+ self.stream.read_bytes(4, self._on_masking_key);
+
+ def _on_masking_key(self, data):
+ self._frame_mask = array.array("B", data)
+ self.stream.read_bytes(self._frame_length, self._on_frame_data)
+
+ def _on_frame_data(self, data):
+ unmasked = array.array("B", data)
+ for i in xrange(len(data)):
+ unmasked[i] = unmasked[i] ^ self._frame_mask[i % 4]
+
+ if self._frame_opcode_is_control:
+ # control frames may be interleaved with a series of fragmented
+ # data frames, so control frames must not interact with
+ # self._fragmented_*
+ if not self._final_frame:
+ # control frames must not be fragmented
+ self._abort()
+ return
+ opcode = self._frame_opcode
+ elif self._frame_opcode == 0: # continuation frame
+ if self._fragmented_message_buffer is None:
+ # nothing to continue
+ self._abort()
+ return
+ self._fragmented_message_buffer += unmasked
+ if self._final_frame:
+ opcode = self._fragmented_message_opcode
+ unmasked = self._fragmented_message_buffer
+ self._fragmented_message_buffer = None
+ else: # start of new data message
+ if self._fragmented_message_buffer is not None:
+ # can't start new message until the old one is finished
+ self._abort()
+ return
+ if self._final_frame:
+ opcode = self._frame_opcode
+ else:
+ self._fragmented_message_opcode = self._frame_opcode
+ self._fragmented_message_buffer = unmasked
+
+ if self._final_frame:
+ self._handle_message(opcode, unmasked.tostring())
+
+ if not self.client_terminated:
+ self._receive_frame()
+
+
+ def _handle_message(self, opcode, data):
+ if self.client_terminated: return
+
+ if opcode == 0x1:
+ # UTF-8 data
+ try:
+ decoded = data.decode("utf-8")
+ except UnicodeDecodeError:
+ self._abort()
+ return
+ self.async_callback(self.handler.on_message)(decoded)
+ elif opcode == 0x2:
+ # Binary data
+ self.async_callback(self.handler.on_message)(data)
+ elif opcode == 0x8:
+ # Close
+ self.client_terminated = True
+ self.close()
+ elif opcode == 0x9:
+ # Ping
+ self._write_frame(True, 0xA, data)
+ elif opcode == 0xA:
+ # Pong
+ pass
+ else:
+ self._abort()
+
+ def close(self):
+ """Closes the WebSocket connection."""
+ if not self.server_terminated:
+ if not self.stream.closed():
+ self._write_frame(True, 0x8, b(""))
+ self.server_terminated = True
+ if self.client_terminated:
+ if self._waiting is not None:
+ self.stream.io_loop.remove_timeout(self._waiting)
+ self._waiting = None
+ self.stream.close()
+ elif self._waiting is None:
+ # Give the client a few seconds to complete a clean shutdown,
+ # otherwise just close the connection.
+ self._waiting = self.stream.io_loop.add_timeout(
+ time.time() + 5, self._abort)
diff --git a/web/index.html b/web/index.html
index 0df51fe..ebc37fb 100644
--- a/web/index.html
+++ b/web/index.html
@@ -53,7 +53,7 @@
<div id="uploadarea">
<table>
<tr><td>
- <form action="/datastore/upload" method="post" enctype="multipart/form-data">
+ <form action="/upload" method="post" enctype="multipart/form-data">
Add from my Journal:<br>
<input type="file" name="journal_item" size="30">
<input type="submit" value="Send">