Merge pull request #964 from JackDandy/feature/UpdateTornado

Update Tornado Web Server 4.5.dev1 (92f29b8) to 4.5.1 (79b2683)
This commit is contained in:
JackDandy 2017-08-22 13:17:31 +01:00 committed by GitHub
commit 18d8bac4a1
44 changed files with 764 additions and 353 deletions

View file

@ -24,7 +24,7 @@
* Update SimpleJSON library 3.8.1 (6022794) to 3.10.0 (c52efea) * Update SimpleJSON library 3.8.1 (6022794) to 3.10.0 (c52efea)
* Update Six compatibility library 1.10.0 (r405) to 1.10.0 (r433) * Update Six compatibility library 1.10.0 (r405) to 1.10.0 (r433)
* Update socks from SocksiPy 1.0 to PySocks 1.6.5 (b4323df) * Update socks from SocksiPy 1.0 to PySocks 1.6.5 (b4323df)
* Update Tornado Web Server 4.5.dev1 (92f29b8) to 4.5.dev1 (38e493e) * Update Tornado Web Server 4.5.dev1 (92f29b8) to 4.5.1 (79b2683)
* Update unidecode library 0.04.18 to 0.04.20 (1e18d98) * Update unidecode library 0.04.18 to 0.04.20 (1e18d98)
* Update xmltodict library 0.9.2 (eac0031) to 0.10.2 (375d3a6) * Update xmltodict library 0.9.2 (eac0031) to 0.10.2 (375d3a6)
* Update Bootstrap 3.2.0 to 3.3.7 * Update Bootstrap 3.2.0 to 3.3.7
@ -87,6 +87,7 @@
* Change restart/shutdown to use updated jQuery * Change restart/shutdown to use updated jQuery
* Remove AlphaReign torrent provider * Remove AlphaReign torrent provider
* Update cachecontrol library 0.11.5 to 0.11.7 (3b3b776) * Update cachecontrol library 0.11.5 to 0.11.7 (3b3b776)
* Update Tornado Web Server 4.5.dev1 (92f29b8) to 4.5.dev1 (38e493e)
### 0.12.26 (2017-08-20 13:05:00 UTC) ### 0.12.26 (2017-08-20 13:05:00 UTC)

View file

@ -16,7 +16,7 @@
"""The Tornado web server and tools.""" """The Tornado web server and tools."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
# version is a human-readable version number. # version is a human-readable version number.
@ -25,5 +25,5 @@ from __future__ import absolute_import, division, print_function, with_statement
# is zero for an official release, positive for a development branch, # is zero for an official release, positive for a development branch,
# or negative for a release candidate or beta (after the base version # or negative for a release candidate or beta (after the base version
# number has been incremented) # number has been incremented)
version = "4.5.dev1" version = "4.5.1"
version_info = (4, 5, 0, -100) version_info = (4, 5, 1, 0)

View file

@ -17,7 +17,7 @@
"""Data used by the tornado.locale module.""" """Data used by the tornado.locale module."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
LOCALE_NAMES = { LOCALE_NAMES = {
"af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"}, "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},

View file

@ -65,7 +65,7 @@ Example usage for Google OAuth:
errors are more consistently reported through the ``Future`` interfaces. errors are more consistently reported through the ``Future`` interfaces.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import base64 import base64
import binascii import binascii
@ -954,6 +954,20 @@ class FacebookGraphMixin(OAuth2Mixin):
.. testoutput:: .. testoutput::
:hide: :hide:
This method returns a dictionary which may contain the following fields:
* ``access_token``, a string which may be passed to `facebook_request`
* ``session_expires``, an integer encoded as a string representing
the time until the access token expires in seconds. This field should
be used like ``int(user['session_expires'])``; in a future version of
Tornado it will change from a string to an integer.
* ``id``, ``name``, ``first_name``, ``last_name``, ``locale``, ``picture``,
``link``, plus any fields named in the ``extra_fields`` argument. These
fields are copied from the Facebook graph API `user object <https://developers.facebook.com/docs/graph-api/reference/user>`_
.. versionchanged:: 4.5
The ``session_expires`` field was updated to support changes made to the
Facebook API in March 2017.
""" """
http = self.get_auth_http_client() http = self.get_auth_http_client()
args = { args = {
@ -978,10 +992,10 @@ class FacebookGraphMixin(OAuth2Mixin):
future.set_exception(AuthError('Facebook auth error: %s' % str(response))) future.set_exception(AuthError('Facebook auth error: %s' % str(response)))
return return
args = urlparse.parse_qs(escape.native_str(response.body)) args = escape.json_decode(response.body)
session = { session = {
"access_token": args["access_token"][-1], "access_token": args.get("access_token"),
"expires": args.get("expires") "expires_in": args.get("expires_in")
} }
self.facebook_request( self.facebook_request(
@ -1004,7 +1018,12 @@ class FacebookGraphMixin(OAuth2Mixin):
for field in fields: for field in fields:
fieldmap[field] = user.get(field) fieldmap[field] = user.get(field)
fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")}) # session_expires is converted to str for compatibility with
# older versions in which the server used url-encoding and
# this code simply returned the string verbatim.
# This should change in Tornado 5.0.
fieldmap.update({"access_token": session["access_token"],
"session_expires": str(session.get("expires_in"))})
future.set_result(fieldmap) future.set_result(fieldmap)
@_auth_return_future @_auth_return_future

View file

@ -45,7 +45,7 @@ incorrectly.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import os import os
import sys import sys
@ -103,10 +103,6 @@ except ImportError:
# os.execv is broken on Windows and can't properly parse command line # os.execv is broken on Windows and can't properly parse command line
# arguments and executable name if they contain whitespaces. subprocess # arguments and executable name if they contain whitespaces. subprocess
# fixes that behavior. # fixes that behavior.
# This distinction is also important because when we use execv, we want to
# close the IOLoop and all its file descriptors, to guard against any
# file descriptors that were not set CLOEXEC. When execv is not available,
# we must not close the IOLoop because we want the process to exit cleanly.
_has_execv = sys.platform != 'win32' _has_execv = sys.platform != 'win32'
_watched_files = set() _watched_files = set()
@ -127,8 +123,6 @@ def start(io_loop=None, check_time=500):
_io_loops[io_loop] = True _io_loops[io_loop] = True
if len(_io_loops) > 1: if len(_io_loops) > 1:
gen_log.warning("tornado.autoreload started more than once in the same process") gen_log.warning("tornado.autoreload started more than once in the same process")
if _has_execv:
add_reload_hook(functools.partial(io_loop.close, all_fds=True))
modify_times = {} modify_times = {}
callback = functools.partial(_reload_on_update, modify_times) callback = functools.partial(_reload_on_update, modify_times)
scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop) scheduler = ioloop.PeriodicCallback(callback, check_time, io_loop=io_loop)
@ -249,6 +243,7 @@ def _reload():
# unwind, so just exit uncleanly. # unwind, so just exit uncleanly.
os._exit(0) os._exit(0)
_USAGE = """\ _USAGE = """\
Usage: Usage:
python -m tornado.autoreload -m module.to.run [args...] python -m tornado.autoreload -m module.to.run [args...]

View file

@ -21,7 +21,7 @@ a mostly-compatible `Future` class designed for use from coroutines,
as well as some utility functions for interacting with the as well as some utility functions for interacting with the
`concurrent.futures` package. `concurrent.futures` package.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import functools import functools
import platform import platform
@ -234,7 +234,10 @@ class Future(object):
if self._result is not None: if self._result is not None:
return self._result return self._result
if self._exc_info is not None: if self._exc_info is not None:
raise_exc_info(self._exc_info) try:
raise_exc_info(self._exc_info)
finally:
self = None
self._check_done() self._check_done()
return self._result return self._result
@ -340,6 +343,7 @@ class Future(object):
app_log.error('Future %r exception was never retrieved: %s', app_log.error('Future %r exception was never retrieved: %s',
self, ''.join(tb).rstrip()) self, ''.join(tb).rstrip())
TracebackFuture = Future TracebackFuture = Future
if futures is None: if futures is None:
@ -364,6 +368,7 @@ class DummyExecutor(object):
def shutdown(self, wait=True): def shutdown(self, wait=True):
pass pass
dummy_executor = DummyExecutor() dummy_executor = DummyExecutor()

View file

@ -16,7 +16,7 @@
"""Non-blocking HTTP client implementation using pycurl.""" """Non-blocking HTTP client implementation using pycurl."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import collections import collections
import functools import functools
@ -278,9 +278,9 @@ class CurlAsyncHTTPClient(AsyncHTTPClient):
if curl_log.isEnabledFor(logging.DEBUG): if curl_log.isEnabledFor(logging.DEBUG):
curl.setopt(pycurl.VERBOSE, 1) curl.setopt(pycurl.VERBOSE, 1)
curl.setopt(pycurl.DEBUGFUNCTION, self._curl_debug) curl.setopt(pycurl.DEBUGFUNCTION, self._curl_debug)
if hasattr(pycurl,'PROTOCOLS'): # PROTOCOLS first appeared in pycurl 7.19.5 (2014-07-12) if hasattr(pycurl, 'PROTOCOLS'): # PROTOCOLS first appeared in pycurl 7.19.5 (2014-07-12)
curl.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTP|pycurl.PROTO_HTTPS) curl.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)
curl.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP|pycurl.PROTO_HTTPS) curl.setopt(pycurl.REDIR_PROTOCOLS, pycurl.PROTO_HTTP | pycurl.PROTO_HTTPS)
return curl return curl
def _curl_setup_request(self, curl, request, buffer, headers): def _curl_setup_request(self, curl, request, buffer, headers):

View file

@ -20,7 +20,7 @@ Also includes a few other miscellaneous string manipulation functions that
have crept in over time. have crept in over time.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import json import json
import re import re
@ -199,6 +199,7 @@ def utf8(value):
) )
return value.encode("utf-8") return value.encode("utf-8")
_TO_UNICODE_TYPES = (unicode_type, type(None)) _TO_UNICODE_TYPES = (unicode_type, type(None))
@ -216,6 +217,7 @@ def to_unicode(value):
) )
return value.decode("utf-8") return value.decode("utf-8")
# to_unicode was previously named _unicode not because it was private, # to_unicode was previously named _unicode not because it was private,
# but to avoid conflicts with the built-in unicode() function/type # but to avoid conflicts with the built-in unicode() function/type
_unicode = to_unicode _unicode = to_unicode
@ -264,6 +266,7 @@ def recursive_unicode(obj):
else: else:
return obj return obj
# I originally used the regex from # I originally used the regex from
# http://daringfireball.net/2010/07/improved_regex_for_matching_urls # http://daringfireball.net/2010/07/improved_regex_for_matching_urls
# but it gets all exponential on certain patterns (such as too many trailing # but it gets all exponential on certain patterns (such as too many trailing
@ -391,4 +394,5 @@ def _build_unicode_map():
unicode_map[name] = unichr(value) unicode_map[name] = unichr(value)
return unicode_map return unicode_map
_HTML_UNICODE_MAP = _build_unicode_map() _HTML_UNICODE_MAP = _build_unicode_map()

View file

@ -74,7 +74,7 @@ See the `convert_yielded` function to extend this mechanism.
via ``singledispatch``. via ``singledispatch``.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import collections import collections
import functools import functools
@ -245,6 +245,7 @@ def coroutine(func, replace_callback=True):
""" """
return _make_coroutine_wrapper(func, replace_callback=True) return _make_coroutine_wrapper(func, replace_callback=True)
# Ties lifetime of runners to their result futures. Github Issue #1769 # Ties lifetime of runners to their result futures. Github Issue #1769
# Generators, like any object in Python, must be strong referenced # Generators, like any object in Python, must be strong referenced
# in order to not be cleaned up by the garbage collector. When using # in order to not be cleaned up by the garbage collector. When using
@ -264,6 +265,7 @@ def coroutine(func, replace_callback=True):
# Runner alive. # Runner alive.
_futures_to_runners = weakref.WeakKeyDictionary() _futures_to_runners = weakref.WeakKeyDictionary()
def _make_coroutine_wrapper(func, replace_callback): def _make_coroutine_wrapper(func, replace_callback):
"""The inner workings of ``@gen.coroutine`` and ``@gen.engine``. """The inner workings of ``@gen.coroutine`` and ``@gen.engine``.
@ -315,6 +317,7 @@ def _make_coroutine_wrapper(func, replace_callback):
future.set_exc_info(sys.exc_info()) future.set_exc_info(sys.exc_info())
else: else:
_futures_to_runners[future] = Runner(result, future, yielded) _futures_to_runners[future] = Runner(result, future, yielded)
yielded = None
try: try:
return future return future
finally: finally:
@ -338,6 +341,8 @@ def _make_coroutine_wrapper(func, replace_callback):
def is_coroutine_function(func): def is_coroutine_function(func):
"""Return whether *func* is a coroutine function, i.e. a function """Return whether *func* is a coroutine function, i.e. a function
wrapped with `~.gen.coroutine`. wrapped with `~.gen.coroutine`.
.. versionadded:: 4.5
""" """
return getattr(func, '__tornado_coroutine__', False) return getattr(func, '__tornado_coroutine__', False)
@ -715,6 +720,7 @@ def multi(children, quiet_exceptions=()):
else: else:
return multi_future(children, quiet_exceptions=quiet_exceptions) return multi_future(children, quiet_exceptions=quiet_exceptions)
Multi = multi Multi = multi
@ -960,6 +966,9 @@ coroutines that are likely to yield Futures that are ready instantly.
Usage: ``yield gen.moment`` Usage: ``yield gen.moment``
.. versionadded:: 4.0 .. versionadded:: 4.0
.. deprecated:: 4.5
``yield None`` is now equivalent to ``yield gen.moment``.
""" """
moment.set_result(None) moment.set_result(None)
@ -990,6 +999,7 @@ class Runner(object):
# of the coroutine. # of the coroutine.
self.stack_context_deactivate = None self.stack_context_deactivate = None
if self.handle_yield(first_yielded): if self.handle_yield(first_yielded):
gen = result_future = first_yielded = None
self.run() self.run()
def register_callback(self, key): def register_callback(self, key):
@ -1046,10 +1056,15 @@ class Runner(object):
except Exception: except Exception:
self.had_exception = True self.had_exception = True
exc_info = sys.exc_info() exc_info = sys.exc_info()
future = None
if exc_info is not None: if exc_info is not None:
yielded = self.gen.throw(*exc_info) try:
exc_info = None yielded = self.gen.throw(*exc_info)
finally:
# Break up a reference to itself
# for faster GC on CPython.
exc_info = None
else: else:
yielded = self.gen.send(value) yielded = self.gen.send(value)
@ -1082,6 +1097,7 @@ class Runner(object):
return return
if not self.handle_yield(yielded): if not self.handle_yield(yielded):
return return
yielded = None
finally: finally:
self.running = False self.running = False
@ -1130,8 +1146,12 @@ class Runner(object):
self.future.set_exc_info(sys.exc_info()) self.future.set_exc_info(sys.exc_info())
if not self.future.done() or self.future is moment: if not self.future.done() or self.future is moment:
def inner(f):
# Break a reference cycle to speed GC.
f = None # noqa
self.run()
self.io_loop.add_future( self.io_loop.add_future(
self.future, lambda f: self.run()) self.future, inner)
return False return False
return True return True
@ -1153,6 +1173,7 @@ class Runner(object):
self.stack_context_deactivate() self.stack_context_deactivate()
self.stack_context_deactivate = None self.stack_context_deactivate = None
Arguments = collections.namedtuple('Arguments', ['args', 'kwargs']) Arguments = collections.namedtuple('Arguments', ['args', 'kwargs'])
@ -1172,6 +1193,7 @@ def _argument_adapter(callback):
callback(None) callback(None)
return wrapper return wrapper
# Convert Awaitables into Futures. It is unfortunately possible # Convert Awaitables into Futures. It is unfortunately possible
# to have infinite recursion here if those Awaitables assume that # to have infinite recursion here if those Awaitables assume that
# we're using a different coroutine runner and yield objects # we're using a different coroutine runner and yield objects
@ -1249,7 +1271,9 @@ def convert_yielded(yielded):
.. versionadded:: 4.1 .. versionadded:: 4.1
""" """
# Lists and dicts containing YieldPoints were handled earlier. # Lists and dicts containing YieldPoints were handled earlier.
if isinstance(yielded, (list, dict)): if yielded is None:
return moment
elif isinstance(yielded, (list, dict)):
return multi(yielded) return multi(yielded)
elif is_future(yielded): elif is_future(yielded):
return yielded return yielded
@ -1258,6 +1282,7 @@ def convert_yielded(yielded):
else: else:
raise BadYieldError("yielded unknown object %r" % (yielded,)) raise BadYieldError("yielded unknown object %r" % (yielded,))
if singledispatch is not None: if singledispatch is not None:
convert_yielded = singledispatch(convert_yielded) convert_yielded = singledispatch(convert_yielded)

View file

@ -19,7 +19,7 @@
.. versionadded:: 4.0 .. versionadded:: 4.0
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import re import re
@ -257,6 +257,7 @@ class HTTP1Connection(httputil.HTTPConnection):
if need_delegate_close: if need_delegate_close:
with _ExceptionLoggingContext(app_log): with _ExceptionLoggingContext(app_log):
delegate.on_connection_close() delegate.on_connection_close()
header_future = None
self._clear_callbacks() self._clear_callbacks()
raise gen.Return(True) raise gen.Return(True)
@ -489,7 +490,7 @@ class HTTP1Connection(httputil.HTTPConnection):
elif ("Content-Length" in headers or elif ("Content-Length" in headers or
headers.get("Transfer-Encoding", "").lower() == "chunked" or headers.get("Transfer-Encoding", "").lower() == "chunked" or
getattr(start_line, 'method', None) in ("HEAD", "GET")): getattr(start_line, 'method', None) in ("HEAD", "GET")):
# start_line may be a request or reponse start line; only # start_line may be a request or response start line; only
# the former has a method attribute. # the former has a method attribute.
return connection_header == "keep-alive" return connection_header == "keep-alive"
return False return False

View file

@ -38,7 +38,7 @@ To select ``curl_httpclient``, call `AsyncHTTPClient.configure` at startup::
AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient") AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import functools import functools
import time import time
@ -423,6 +423,9 @@ class HTTPRequest(object):
.. versionadded:: 4.2 .. versionadded:: 4.2
The ``ssl_options`` argument. The ``ssl_options`` argument.
.. versionadded:: 4.5
The ``proxy_auth_mode`` argument.
""" """
# Note that some of these attributes go through property setters # Note that some of these attributes go through property setters
# defined below. # defined below.
@ -670,5 +673,6 @@ def main():
print(native_str(response.body)) print(native_str(response.body))
client.close() client.close()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -26,7 +26,7 @@ class except to start a server at the beginning of the process
to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias. to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import socket import socket
@ -62,6 +62,13 @@ class HTTPServer(TCPServer, Configurable,
if Tornado is run behind an SSL-decoding proxy that does not set one of if Tornado is run behind an SSL-decoding proxy that does not set one of
the supported ``xheaders``. the supported ``xheaders``.
By default, when parsing the ``X-Forwarded-For`` header, Tornado will
select the last (i.e., the closest) address on the list of hosts as the
remote host IP address. To select the next server in the chain, a list of
trusted downstream hosts may be passed as the ``trusted_downstream``
argument. These hosts will be skipped when parsing the ``X-Forwarded-For``
header.
To make this server serve SSL traffic, send the ``ssl_options`` keyword To make this server serve SSL traffic, send the ``ssl_options`` keyword
argument with an `ssl.SSLContext` object. For compatibility with older argument with an `ssl.SSLContext` object. For compatibility with older
versions of Python ``ssl_options`` may also be a dictionary of keyword versions of Python ``ssl_options`` may also be a dictionary of keyword
@ -124,6 +131,9 @@ class HTTPServer(TCPServer, Configurable,
.. versionchanged:: 4.2 .. versionchanged:: 4.2
`HTTPServer` is now a subclass of `tornado.util.Configurable`. `HTTPServer` is now a subclass of `tornado.util.Configurable`.
.. versionchanged:: 4.5
Added the ``trusted_downstream`` argument.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Ignore args to __init__; real initialization belongs in # Ignore args to __init__; real initialization belongs in
@ -138,7 +148,8 @@ class HTTPServer(TCPServer, Configurable,
decompress_request=False, decompress_request=False,
chunk_size=None, max_header_size=None, chunk_size=None, max_header_size=None,
idle_connection_timeout=None, body_timeout=None, idle_connection_timeout=None, body_timeout=None,
max_body_size=None, max_buffer_size=None): max_body_size=None, max_buffer_size=None,
trusted_downstream=None):
self.request_callback = request_callback self.request_callback = request_callback
self.no_keep_alive = no_keep_alive self.no_keep_alive = no_keep_alive
self.xheaders = xheaders self.xheaders = xheaders
@ -149,11 +160,13 @@ class HTTPServer(TCPServer, Configurable,
max_header_size=max_header_size, max_header_size=max_header_size,
header_timeout=idle_connection_timeout or 3600, header_timeout=idle_connection_timeout or 3600,
max_body_size=max_body_size, max_body_size=max_body_size,
body_timeout=body_timeout) body_timeout=body_timeout,
no_keep_alive=no_keep_alive)
TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
max_buffer_size=max_buffer_size, max_buffer_size=max_buffer_size,
read_chunk_size=chunk_size) read_chunk_size=chunk_size)
self._connections = set() self._connections = set()
self.trusted_downstream = trusted_downstream
@classmethod @classmethod
def configurable_base(cls): def configurable_base(cls):
@ -172,7 +185,8 @@ class HTTPServer(TCPServer, Configurable,
def handle_stream(self, stream, address): def handle_stream(self, stream, address):
context = _HTTPRequestContext(stream, address, context = _HTTPRequestContext(stream, address,
self.protocol) self.protocol,
self.trusted_downstream)
conn = HTTP1ServerConnection( conn = HTTP1ServerConnection(
stream, self.conn_params, context) stream, self.conn_params, context)
self._connections.add(conn) self._connections.add(conn)
@ -219,7 +233,7 @@ class _CallableAdapter(httputil.HTTPMessageDelegate):
class _HTTPRequestContext(object): class _HTTPRequestContext(object):
def __init__(self, stream, address, protocol): def __init__(self, stream, address, protocol, trusted_downstream=None):
self.address = address self.address = address
# Save the socket's address family now so we know how to # Save the socket's address family now so we know how to
# interpret self.address even after the stream is closed # interpret self.address even after the stream is closed
@ -243,6 +257,7 @@ class _HTTPRequestContext(object):
self.protocol = "http" self.protocol = "http"
self._orig_remote_ip = self.remote_ip self._orig_remote_ip = self.remote_ip
self._orig_protocol = self.protocol self._orig_protocol = self.protocol
self.trusted_downstream = set(trusted_downstream or [])
def __str__(self): def __str__(self):
if self.address_family in (socket.AF_INET, socket.AF_INET6): if self.address_family in (socket.AF_INET, socket.AF_INET6):
@ -259,7 +274,10 @@ class _HTTPRequestContext(object):
"""Rewrite the ``remote_ip`` and ``protocol`` fields.""" """Rewrite the ``remote_ip`` and ``protocol`` fields."""
# Squid uses X-Forwarded-For, others use X-Real-Ip # Squid uses X-Forwarded-For, others use X-Real-Ip
ip = headers.get("X-Forwarded-For", self.remote_ip) ip = headers.get("X-Forwarded-For", self.remote_ip)
ip = ip.split(',')[-1].strip() # Skip trusted downstream hosts in X-Forwarded-For list
for ip in (cand.strip() for cand in reversed(ip.split(','))):
if ip not in self.trusted_downstream:
break
ip = headers.get("X-Real-Ip", ip) ip = headers.get("X-Real-Ip", ip)
if netutil.is_valid_ip(ip): if netutil.is_valid_ip(ip):
self.remote_ip = ip self.remote_ip = ip
@ -303,4 +321,5 @@ class _ProxyAdapter(httputil.HTTPMessageDelegate):
def _cleanup(self): def _cleanup(self):
self.connection.context._unapply_xheaders() self.connection.context._unapply_xheaders()
HTTPRequest = httputil.HTTPServerRequest HTTPRequest = httputil.HTTPServerRequest

View file

@ -20,7 +20,7 @@ This module also defines the `HTTPServerRequest` class which is exposed
via `tornado.web.RequestHandler.request`. via `tornado.web.RequestHandler.request`.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import calendar import calendar
import collections import collections
@ -38,11 +38,12 @@ from tornado.util import ObjectDict, PY3
if PY3: if PY3:
import http.cookies as Cookie import http.cookies as Cookie
from http.client import responses from http.client import responses
from urllib.parse import urlencode from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
else: else:
import Cookie import Cookie
from httplib import responses from httplib import responses
from urllib import urlencode from urllib import urlencode
from urlparse import urlparse, urlunparse, parse_qsl
# responses is unused in this file, but we re-export it to other files. # responses is unused in this file, but we re-export it to other files.
@ -98,6 +99,7 @@ class _NormalizedHeaderCache(dict):
del self[old_key] del self[old_key]
return normalized return normalized
_normalized_headers = _NormalizedHeaderCache(1000) _normalized_headers = _NormalizedHeaderCache(1000)
@ -601,11 +603,28 @@ def url_concat(url, args):
>>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")]) >>> url_concat("http://example.com/foo?a=b", [("c", "d"), ("c", "d2")])
'http://example.com/foo?a=b&c=d&c=d2' 'http://example.com/foo?a=b&c=d&c=d2'
""" """
if not args: if args is None:
return url return url
if url[-1] not in ('?', '&'): parsed_url = urlparse(url)
url += '&' if ('?' in url) else '?' if isinstance(args, dict):
return url + urlencode(args) parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True)
parsed_query.extend(args.items())
elif isinstance(args, list) or isinstance(args, tuple):
parsed_query = parse_qsl(parsed_url.query, keep_blank_values=True)
parsed_query.extend(args)
else:
err = "'args' parameter should be dict, list or tuple. Not {0}".format(
type(args))
raise TypeError(err)
final_query = urlencode(parsed_query)
url = urlunparse((
parsed_url[0],
parsed_url[1],
parsed_url[2],
parsed_url[3],
final_query,
parsed_url[5]))
return url
class HTTPFile(ObjectDict): class HTTPFile(ObjectDict):
@ -920,10 +939,12 @@ def split_host_and_port(netloc):
port = None port = None
return (host, port) return (host, port)
_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") _OctalPatt = re.compile(r"\\[0-3][0-7][0-7]")
_QuotePatt = re.compile(r"[\\].") _QuotePatt = re.compile(r"[\\].")
_nulljoin = ''.join _nulljoin = ''.join
def _unquote_cookie(str): def _unquote_cookie(str):
"""Handle double quotes and escaping in cookie values. """Handle double quotes and escaping in cookie values.
@ -965,11 +986,11 @@ def _unquote_cookie(str):
k = q_match.start(0) k = q_match.start(0)
if q_match and (not o_match or k < j): # QuotePatt matched if q_match and (not o_match or k < j): # QuotePatt matched
res.append(str[i:k]) res.append(str[i:k])
res.append(str[k+1]) res.append(str[k + 1])
i = k + 2 i = k + 2
else: # OctalPatt matched else: # OctalPatt matched
res.append(str[i:j]) res.append(str[i:j])
res.append(chr(int(str[j+1:j+4], 8))) res.append(chr(int(str[j + 1:j + 4], 8)))
i = j + 4 i = j + 4
return _nulljoin(res) return _nulljoin(res)

View file

@ -26,7 +26,7 @@ In addition to I/O events, the `IOLoop` can also schedule time-based events.
`IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`. `IOLoop.add_timeout` is a non-blocking alternative to `time.sleep`.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import collections import collections
import datetime import datetime
@ -715,7 +715,7 @@ class PollIOLoop(IOLoop):
self._closing = True self._closing = True
self.remove_handler(self._waker.fileno()) self.remove_handler(self._waker.fileno())
if all_fds: if all_fds:
for fd, handler in self._handlers.values(): for fd, handler in list(self._handlers.values()):
self.close_fd(fd) self.close_fd(fd)
self._waker.close() self._waker.close()
self._impl.close() self._impl.close()

View file

@ -24,7 +24,7 @@ Contents:
* `PipeIOStream`: Pipe-based IOStream implementation. * `PipeIOStream`: Pipe-based IOStream implementation.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import collections import collections
import errno import errno
@ -82,6 +82,8 @@ _ERRNO_INPROGRESS = (errno.EINPROGRESS,)
if hasattr(errno, "WSAEINPROGRESS"): if hasattr(errno, "WSAEINPROGRESS"):
_ERRNO_INPROGRESS += (errno.WSAEINPROGRESS,) # type: ignore _ERRNO_INPROGRESS += (errno.WSAEINPROGRESS,) # type: ignore
_WINDOWS = sys.platform.startswith('win')
class StreamClosedError(IOError): class StreamClosedError(IOError):
"""Exception raised by `IOStream` methods when the stream is closed. """Exception raised by `IOStream` methods when the stream is closed.
@ -158,11 +160,16 @@ class BaseIOStream(object):
self.max_buffer_size // 2) self.max_buffer_size // 2)
self.max_write_buffer_size = max_write_buffer_size self.max_write_buffer_size = max_write_buffer_size
self.error = None self.error = None
self._read_buffer = collections.deque() self._read_buffer = bytearray()
self._write_buffer = collections.deque() self._read_buffer_pos = 0
self._read_buffer_size = 0 self._read_buffer_size = 0
self._write_buffer = bytearray()
self._write_buffer_pos = 0
self._write_buffer_size = 0 self._write_buffer_size = 0
self._write_buffer_frozen = False self._write_buffer_frozen = False
self._total_write_index = 0
self._total_write_done_index = 0
self._pending_writes_while_frozen = []
self._read_delimiter = None self._read_delimiter = None
self._read_regex = None self._read_regex = None
self._read_max_bytes = None self._read_max_bytes = None
@ -173,7 +180,7 @@ class BaseIOStream(object):
self._read_future = None self._read_future = None
self._streaming_callback = None self._streaming_callback = None
self._write_callback = None self._write_callback = None
self._write_future = None self._write_futures = collections.deque()
self._close_callback = None self._close_callback = None
self._connect_callback = None self._connect_callback = None
self._connect_future = None self._connect_future = None
@ -367,36 +374,37 @@ class BaseIOStream(object):
If no ``callback`` is given, this method returns a `.Future` that If no ``callback`` is given, this method returns a `.Future` that
resolves (with a result of ``None``) when the write has been resolves (with a result of ``None``) when the write has been
completed. If `write` is called again before that `.Future` has completed.
resolved, the previous future will be orphaned and will never resolve.
The ``data`` argument may be of type `bytes` or `memoryview`.
.. versionchanged:: 4.0 .. versionchanged:: 4.0
Now returns a `.Future` if no callback is given. Now returns a `.Future` if no callback is given.
.. versionchanged:: 4.5
Added support for `memoryview` arguments.
""" """
assert isinstance(data, bytes)
self._check_closed() self._check_closed()
# We use bool(_write_buffer) as a proxy for write_buffer_size>0,
# so never put empty strings in the buffer.
if data: if data:
if (self.max_write_buffer_size is not None and if (self.max_write_buffer_size is not None and
self._write_buffer_size + len(data) > self.max_write_buffer_size): self._write_buffer_size + len(data) > self.max_write_buffer_size):
raise StreamBufferFullError("Reached maximum write buffer size") raise StreamBufferFullError("Reached maximum write buffer size")
# Break up large contiguous strings before inserting them in the if self._write_buffer_frozen:
# write buffer, so we don't have to recopy the entire thing self._pending_writes_while_frozen.append(data)
# as we slice off pieces to send to the socket. else:
WRITE_BUFFER_CHUNK_SIZE = 128 * 1024 self._write_buffer += data
for i in range(0, len(data), WRITE_BUFFER_CHUNK_SIZE): self._write_buffer_size += len(data)
self._write_buffer.append(data[i:i + WRITE_BUFFER_CHUNK_SIZE]) self._total_write_index += len(data)
self._write_buffer_size += len(data)
if callback is not None: if callback is not None:
self._write_callback = stack_context.wrap(callback) self._write_callback = stack_context.wrap(callback)
future = None future = None
else: else:
future = self._write_future = TracebackFuture() future = TracebackFuture()
future.add_done_callback(lambda f: f.exception()) future.add_done_callback(lambda f: f.exception())
self._write_futures.append((self._total_write_index, future))
if not self._connecting: if not self._connecting:
self._handle_write() self._handle_write()
if self._write_buffer: if self._write_buffer_size:
self._add_io_state(self.io_loop.WRITE) self._add_io_state(self.io_loop.WRITE)
self._maybe_add_error_listener() self._maybe_add_error_listener()
return future return future
@ -445,9 +453,8 @@ class BaseIOStream(object):
if self._read_future is not None: if self._read_future is not None:
futures.append(self._read_future) futures.append(self._read_future)
self._read_future = None self._read_future = None
if self._write_future is not None: futures += [future for _, future in self._write_futures]
futures.append(self._write_future) self._write_futures.clear()
self._write_future = None
if self._connect_future is not None: if self._connect_future is not None:
futures.append(self._connect_future) futures.append(self._connect_future)
self._connect_future = None self._connect_future = None
@ -466,6 +473,7 @@ class BaseIOStream(object):
# if the IOStream object is kept alive by a reference cycle. # if the IOStream object is kept alive by a reference cycle.
# TODO: Clear the read buffer too; it currently breaks some tests. # TODO: Clear the read buffer too; it currently breaks some tests.
self._write_buffer = None self._write_buffer = None
self._write_buffer_size = 0
def reading(self): def reading(self):
"""Returns true if we are currently reading from the stream.""" """Returns true if we are currently reading from the stream."""
@ -473,7 +481,7 @@ class BaseIOStream(object):
def writing(self): def writing(self):
"""Returns true if we are currently writing to the stream.""" """Returns true if we are currently writing to the stream."""
return bool(self._write_buffer) return self._write_buffer_size > 0
def closed(self): def closed(self):
"""Returns true if the stream has been closed.""" """Returns true if the stream has been closed."""
@ -650,7 +658,7 @@ class BaseIOStream(object):
except Exception as e: except Exception as e:
if 1 != e.errno: if 1 != e.errno:
gen_log.warning("error on read: %s" % e) gen_log.warning("error on read: %s" % e)
self.close(exc_info=True) self.close(exc_info=e)
return return
if pos is not None: if pos is not None:
self._read_from_buffer(pos) self._read_from_buffer(pos)
@ -744,7 +752,7 @@ class BaseIOStream(object):
break break
if chunk is None: if chunk is None:
return 0 return 0
self._read_buffer.append(chunk) self._read_buffer += chunk
self._read_buffer_size += len(chunk) self._read_buffer_size += len(chunk)
if self._read_buffer_size > self.max_buffer_size: if self._read_buffer_size > self.max_buffer_size:
gen_log.error("Reached maximum read buffer size") gen_log.error("Reached maximum read buffer size")
@ -792,30 +800,25 @@ class BaseIOStream(object):
# since large merges are relatively expensive and get undone in # since large merges are relatively expensive and get undone in
# _consume(). # _consume().
if self._read_buffer: if self._read_buffer:
while True: loc = self._read_buffer.find(self._read_delimiter,
loc = self._read_buffer[0].find(self._read_delimiter) self._read_buffer_pos)
if loc != -1: if loc != -1:
delimiter_len = len(self._read_delimiter) loc -= self._read_buffer_pos
self._check_max_bytes(self._read_delimiter, delimiter_len = len(self._read_delimiter)
loc + delimiter_len) self._check_max_bytes(self._read_delimiter,
return loc + delimiter_len loc + delimiter_len)
if len(self._read_buffer) == 1: return loc + delimiter_len
break
_double_prefix(self._read_buffer)
self._check_max_bytes(self._read_delimiter, self._check_max_bytes(self._read_delimiter,
len(self._read_buffer[0])) self._read_buffer_size)
elif self._read_regex is not None: elif self._read_regex is not None:
if self._read_buffer: if self._read_buffer:
while True: m = self._read_regex.search(self._read_buffer,
m = self._read_regex.search(self._read_buffer[0]) self._read_buffer_pos)
if m is not None: if m is not None:
self._check_max_bytes(self._read_regex, m.end()) loc = m.end() - self._read_buffer_pos
return m.end() self._check_max_bytes(self._read_regex, loc)
if len(self._read_buffer) == 1: return loc
break self._check_max_bytes(self._read_regex, self._read_buffer_size)
_double_prefix(self._read_buffer)
self._check_max_bytes(self._read_regex,
len(self._read_buffer[0]))
return None return None
def _check_max_bytes(self, delimiter, size): def _check_max_bytes(self, delimiter, size):
@ -825,35 +828,56 @@ class BaseIOStream(object):
"delimiter %r not found within %d bytes" % ( "delimiter %r not found within %d bytes" % (
delimiter, self._read_max_bytes)) delimiter, self._read_max_bytes))
def _freeze_write_buffer(self, size):
self._write_buffer_frozen = size
def _unfreeze_write_buffer(self):
self._write_buffer_frozen = False
self._write_buffer += b''.join(self._pending_writes_while_frozen)
self._write_buffer_size += sum(map(len, self._pending_writes_while_frozen))
self._pending_writes_while_frozen[:] = []
def _got_empty_write(self, size):
"""
Called when a non-blocking write() failed writing anything.
Can be overridden in subclasses.
"""
def _handle_write(self): def _handle_write(self):
while self._write_buffer: while self._write_buffer_size:
assert self._write_buffer_size >= 0
try: try:
if not self._write_buffer_frozen: start = self._write_buffer_pos
if self._write_buffer_frozen:
size = self._write_buffer_frozen
elif _WINDOWS:
# On windows, socket.send blows up if given a # On windows, socket.send blows up if given a
# write buffer that's too large, instead of just # write buffer that's too large, instead of just
# returning the number of bytes it was able to # returning the number of bytes it was able to
# process. Therefore we must not call socket.send # process. Therefore we must not call socket.send
# with more than 128KB at a time. # with more than 128KB at a time.
_merge_prefix(self._write_buffer, 128 * 1024) size = 128 * 1024
num_bytes = self.write_to_fd(self._write_buffer[0]) else:
size = self._write_buffer_size
num_bytes = self.write_to_fd(
memoryview(self._write_buffer)[start:start + size])
if num_bytes == 0: if num_bytes == 0:
# With OpenSSL, if we couldn't write the entire buffer, self._got_empty_write(size)
# 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 break
self._write_buffer_frozen = False self._write_buffer_pos += num_bytes
_merge_prefix(self._write_buffer, num_bytes)
self._write_buffer.popleft()
self._write_buffer_size -= num_bytes self._write_buffer_size -= num_bytes
# Amortized O(1) shrink
# (this heuristic is implemented natively in Python 3.4+
# but is replicated here for Python 2)
if self._write_buffer_pos > self._write_buffer_size:
del self._write_buffer[:self._write_buffer_pos]
self._write_buffer_pos = 0
if self._write_buffer_frozen:
self._unfreeze_write_buffer()
self._total_write_done_index += num_bytes
except (socket.error, IOError, OSError) as e: except (socket.error, IOError, OSError) as e:
if e.args[0] in _ERRNO_WOULDBLOCK: if e.args[0] in _ERRNO_WOULDBLOCK:
self._write_buffer_frozen = True self._got_empty_write(size)
break break
else: else:
if not self._is_connreset(e): if not self._is_connreset(e):
@ -864,22 +888,38 @@ class BaseIOStream(object):
self.fileno(), e) self.fileno(), e)
self.close(exc_info=True) self.close(exc_info=True)
return return
if not self._write_buffer:
while self._write_futures:
index, future = self._write_futures[0]
if index > self._total_write_done_index:
break
self._write_futures.popleft()
future.set_result(None)
if not self._write_buffer_size:
if self._write_callback: if self._write_callback:
callback = self._write_callback callback = self._write_callback
self._write_callback = None self._write_callback = None
self._run_callback(callback) self._run_callback(callback)
if self._write_future:
future = self._write_future
self._write_future = None
future.set_result(None)
def _consume(self, loc): def _consume(self, loc):
# Consume loc bytes from the read buffer and return them
if loc == 0: if loc == 0:
return b"" return b""
_merge_prefix(self._read_buffer, loc) assert loc <= self._read_buffer_size
# Slice the bytearray buffer into bytes, without intermediate copying
b = (memoryview(self._read_buffer)
[self._read_buffer_pos:self._read_buffer_pos + loc]
).tobytes()
self._read_buffer_pos += loc
self._read_buffer_size -= loc self._read_buffer_size -= loc
return self._read_buffer.popleft() # Amortized O(1) shrink
# (this heuristic is implemented natively in Python 3.4+
# but is replicated here for Python 2)
if self._read_buffer_pos > self._read_buffer_size:
del self._read_buffer[:self._read_buffer_pos]
self._read_buffer_pos = 0
return b
def _check_closed(self): def _check_closed(self):
if self.closed(): if self.closed():
@ -1125,7 +1165,7 @@ class IOStream(BaseIOStream):
suitably-configured `ssl.SSLContext` to disable. suitably-configured `ssl.SSLContext` to disable.
""" """
if (self._read_callback or self._read_future or if (self._read_callback or self._read_future or
self._write_callback or self._write_future or self._write_callback or self._write_futures or
self._connect_callback or self._connect_future or self._connect_callback or self._connect_future or
self._pending_callbacks or self._closed or self._pending_callbacks or self._closed or
self._read_buffer or self._write_buffer): self._read_buffer or self._write_buffer):
@ -1252,6 +1292,17 @@ class SSLIOStream(IOStream):
def writing(self): def writing(self):
return self._handshake_writing or super(SSLIOStream, self).writing() return self._handshake_writing or super(SSLIOStream, self).writing()
def _got_empty_write(self, size):
# 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._freeze_write_buffer(size)
def _do_ssl_handshake(self): def _do_ssl_handshake(self):
# Based on code from test_ssl.py in the python stdlib # Based on code from test_ssl.py in the python stdlib
try: try:
@ -1499,53 +1550,6 @@ class PipeIOStream(BaseIOStream):
return chunk return chunk
def _double_prefix(deque):
"""Grow by doubling, but don't split the second chunk just because the
first one is small.
"""
new_len = max(len(deque[0]) * 2,
(len(deque[0]) + len(deque[1])))
_merge_prefix(deque, new_len)
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(): def doctests():
import doctest import doctest
return doctest.DocTestSuite() return doctest.DocTestSuite()

View file

@ -39,7 +39,7 @@ supported by `gettext` and related tools). If neither method is called,
the `Locale.translate` method will simply return the original string. the `Locale.translate` method will simply return the original string.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import codecs import codecs
import csv import csv
@ -187,7 +187,7 @@ def load_gettext_translations(directory, domain):
{directory}/{lang}/LC_MESSAGES/{domain}.mo {directory}/{lang}/LC_MESSAGES/{domain}.mo
Three steps are required to have you app translated: Three steps are required to have your app translated:
1. Generate POT translation file:: 1. Generate POT translation file::

View file

@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import collections import collections

View file

@ -28,7 +28,7 @@ These streams may be configured independently using the standard library's
`logging` module. For example, you may wish to send ``tornado.access`` logs `logging` module. For example, you may wish to send ``tornado.access`` logs
to a separate file for analysis. to a separate file for analysis.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import logging import logging
import logging.handlers import logging.handlers
@ -37,6 +37,11 @@ import sys
from tornado.escape import _unicode from tornado.escape import _unicode
from tornado.util import unicode_type, basestring_type from tornado.util import unicode_type, basestring_type
try:
import colorama
except ImportError:
colorama = None
try: try:
import curses # type: ignore import curses # type: ignore
except ImportError: except ImportError:
@ -49,15 +54,21 @@ gen_log = logging.getLogger("tornado.general")
def _stderr_supports_color(): def _stderr_supports_color():
color = False try:
if curses and hasattr(sys.stderr, 'isatty') and sys.stderr.isatty(): if hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
try: if curses:
curses.setupterm() curses.setupterm()
if curses.tigetnum("colors") > 0: if curses.tigetnum("colors") > 0:
color = True return True
except Exception: elif colorama:
pass if sys.stderr is getattr(colorama.initialise, 'wrapped_stderr',
return color object()):
return True
except Exception:
# Very broad exception handling because it's always better to
# fall back to non-colored logs than to break at startup.
pass
return False
def _safe_unicode(s): def _safe_unicode(s):
@ -79,6 +90,17 @@ class LogFormatter(logging.Formatter):
This formatter is enabled automatically by This formatter is enabled automatically by
`tornado.options.parse_command_line` or `tornado.options.parse_config_file` `tornado.options.parse_command_line` or `tornado.options.parse_config_file`
(unless ``--logging=none`` is used). (unless ``--logging=none`` is used).
Color support on Windows versions that do not support ANSI color codes is
enabled by use of the colorama__ library. Applications that wish to use
this must first initialize colorama with a call to ``colorama.init``.
See the colorama documentation for details.
__ https://pypi.python.org/pypi/colorama
.. versionchanged:: 4.5
Added support for ``colorama``. Changed the constructor
signature to be compatible with `logging.config.dictConfig`.
""" """
DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s' DEFAULT_FORMAT = '%(color)s[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]%(end_color)s %(message)s'
DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S' DEFAULT_DATE_FORMAT = '%y%m%d %H:%M:%S'
@ -89,8 +111,8 @@ class LogFormatter(logging.Formatter):
logging.ERROR: 1, # Red logging.ERROR: 1, # Red
} }
def __init__(self, color=True, fmt=DEFAULT_FORMAT, def __init__(self, fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT,
datefmt=DEFAULT_DATE_FORMAT, colors=DEFAULT_COLORS): style='%', color=True, colors=DEFAULT_COLORS):
r""" r"""
:arg bool color: Enables color support. :arg bool color: Enables color support.
:arg string fmt: Log message format. :arg string fmt: Log message format.
@ -111,21 +133,28 @@ class LogFormatter(logging.Formatter):
self._colors = {} self._colors = {}
if color and _stderr_supports_color(): if color and _stderr_supports_color():
# The curses module has some str/bytes confusion in if curses is not None:
# python3. Until version 3.2.3, most methods return # The curses module has some str/bytes confusion in
# bytes, but only accept strings. In addition, we want to # python3. Until version 3.2.3, most methods return
# output these strings with the logging module, which # bytes, but only accept strings. In addition, we want to
# works with unicode strings. The explicit calls to # output these strings with the logging module, which
# unicode() below are harmless in python2 but will do the # works with unicode strings. The explicit calls to
# right conversion in python 3. # unicode() below are harmless in python2 but will do the
fg_color = (curses.tigetstr("setaf") or # right conversion in python 3.
curses.tigetstr("setf") or "") fg_color = (curses.tigetstr("setaf") or
if (3, 0) < sys.version_info < (3, 2, 3): curses.tigetstr("setf") or "")
fg_color = unicode_type(fg_color, "ascii") if (3, 0) < sys.version_info < (3, 2, 3):
fg_color = unicode_type(fg_color, "ascii")
for levelno, code in colors.items(): for levelno, code in colors.items():
self._colors[levelno] = unicode_type(curses.tparm(fg_color, code), "ascii") self._colors[levelno] = unicode_type(curses.tparm(fg_color, code), "ascii")
self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii") self._normal = unicode_type(curses.tigetstr("sgr0"), "ascii")
else:
# If curses is not present (currently we'll only get here for
# colorama on windows), assume hard-coded ANSI color codes.
for levelno, code in colors.items():
self._colors[levelno] = '\033[2;3%dm' % code
self._normal = '\033[0m'
else: else:
self._normal = '' self._normal = ''

View file

@ -16,7 +16,7 @@
"""Miscellaneous network utility code.""" """Miscellaneous network utility code."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import errno import errno
import os import os
@ -200,6 +200,7 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC,
sockets.append(sock) sockets.append(sock)
return sockets return sockets
if hasattr(socket, 'AF_UNIX'): if hasattr(socket, 'AF_UNIX'):
def bind_unix_socket(file, mode=0o600, backlog=_DEFAULT_BACKLOG): def bind_unix_socket(file, mode=0o600, backlog=_DEFAULT_BACKLOG):
"""Creates a listening unix socket. """Creates a listening unix socket.

View file

@ -82,7 +82,7 @@ instances to define isolated sets of options, such as for subcommands.
underscores. underscores.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import datetime import datetime
import numbers import numbers

View file

@ -19,7 +19,7 @@ loops.
Windows. Use the `~asyncio.SelectorEventLoop` instead. Windows. Use the `~asyncio.SelectorEventLoop` instead.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import functools import functools
import tornado.concurrent import tornado.concurrent
@ -30,7 +30,7 @@ from tornado import stack_context
try: try:
# Import the real asyncio module for py33+ first. Older versions of the # Import the real asyncio module for py33+ first. Older versions of the
# trollius backport also use this name. # trollius backport also use this name.
import asyncio # type: ignore import asyncio # type: ignore
except ImportError as e: except ImportError as e:
# Asyncio itself isn't available; see if trollius is (backport to py26+). # Asyncio itself isn't available; see if trollius is (backport to py26+).
try: try:
@ -217,5 +217,6 @@ def to_asyncio_future(tornado_future):
tornado.concurrent.chain_future(tornado_future, af) tornado.concurrent.chain_future(tornado_future, af)
return af return af
if hasattr(convert_yielded, 'register'): if hasattr(convert_yielded, 'register'):
convert_yielded.register(asyncio.Future, to_tornado_future) # type: ignore convert_yielded.register(asyncio.Future, to_tornado_future) # type: ignore

View file

@ -23,7 +23,7 @@ Most code that needs access to this functionality should do e.g.::
from tornado.platform.auto import set_close_exec from tornado.platform.auto import set_close_exec
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import os import os

View file

@ -1,4 +1,4 @@
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import pycares # type: ignore import pycares # type: ignore
import socket import socket

View file

@ -1,5 +1,5 @@
"""Lowest-common-denominator implementations of platform functionality.""" """Lowest-common-denominator implementations of platform functionality."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import errno import errno
import socket import socket
@ -8,6 +8,7 @@ import time
from tornado.platform import interface from tornado.platform import interface
from tornado.util import errno_from_exception from tornado.util import errno_from_exception
def try_close(f): def try_close(f):
# Avoid issue #875 (race condition when using the file in another # Avoid issue #875 (race condition when using the file in another
# thread). # thread).

View file

@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""EPoll-based IOLoop implementation for Linux systems.""" """EPoll-based IOLoop implementation for Linux systems."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import select import select

View file

@ -21,7 +21,7 @@ for other tornado.platform modules. Most code should import the appropriate
implementation from `tornado.platform.auto`. implementation from `tornado.platform.auto`.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
def set_close_exec(fd): def set_close_exec(fd):
@ -62,5 +62,6 @@ class Waker(object):
"""Closes the waker's file descriptor(s).""" """Closes the waker's file descriptor(s)."""
raise NotImplementedError() raise NotImplementedError()
def monotonic_time(): def monotonic_time():
raise NotImplementedError() raise NotImplementedError()

View file

@ -14,7 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""KQueue-based IOLoop implementation for BSD/Mac systems.""" """KQueue-based IOLoop implementation for BSD/Mac systems."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import select import select

View file

@ -16,7 +16,7 @@
"""Posix implementations of platform-specific functionality.""" """Posix implementations of platform-specific functionality."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import fcntl import fcntl
import os import os

View file

@ -17,7 +17,7 @@
Used as a fallback for systems that don't support epoll or kqueue. Used as a fallback for systems that don't support epoll or kqueue.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import select import select

View file

@ -21,7 +21,7 @@ depending on which library's underlying event loop you want to use.
This module has been tested with Twisted versions 11.0.0 and newer. This module has been tested with Twisted versions 11.0.0 and newer.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import datetime import datetime
import functools import functools
@ -574,6 +574,7 @@ class TwistedResolver(Resolver):
] ]
raise gen.Return(result) raise gen.Return(result)
if hasattr(gen.convert_yielded, 'register'): if hasattr(gen.convert_yielded, 'register'):
@gen.convert_yielded.register(Deferred) # type: ignore @gen.convert_yielded.register(Deferred) # type: ignore
def _(d): def _(d):

View file

@ -2,7 +2,7 @@
# for production use. # for production use.
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import ctypes # type: ignore import ctypes # type: ignore
import ctypes.wintypes # type: ignore import ctypes.wintypes # type: ignore

View file

@ -18,7 +18,7 @@
the server into multiple processes and managing subprocesses. the server into multiple processes and managing subprocesses.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import errno import errno
import os import os
@ -355,6 +355,10 @@ class Subprocess(object):
else: else:
assert os.WIFEXITED(status) assert os.WIFEXITED(status)
self.returncode = os.WEXITSTATUS(status) self.returncode = os.WEXITSTATUS(status)
# We've taken over wait() duty from the subprocess.Popen
# object. If we don't inform it of the process's return code,
# it will log a warning at destruction in python 3.6+.
self.proc.returncode = self.returncode
if self._exit_callback: if self._exit_callback:
callback = self._exit_callback callback = self._exit_callback
self._exit_callback = None self._exit_callback = None

View file

@ -12,7 +12,17 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from __future__ import absolute_import, division, print_function, with_statement """Asynchronous queues for coroutines.
.. warning::
Unlike the standard library's `queue` module, the classes defined here
are *not* thread-safe. To use these queues from another thread,
use `.IOLoop.add_callback` to transfer control to the `.IOLoop` thread
before calling any queue methods.
"""
from __future__ import absolute_import, division, print_function
import collections import collections
import heapq import heapq

View file

@ -12,16 +12,23 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Basic routing implementation. """Flexible routing implementation.
Tornado routes HTTP requests to appropriate handlers using `Router` class implementations. Tornado routes HTTP requests to appropriate handlers using `Router`
class implementations. The `tornado.web.Application` class is a
`Router` implementation and may be used directly, or the classes in
this module may be used for additional flexibility. The `RuleRouter`
class can match on more criteria than `.Application`, or the `Router`
interface can be subclassed for maximum customization.
`Router` interface extends `~.httputil.HTTPServerConnectionDelegate` to provide additional `Router` interface extends `~.httputil.HTTPServerConnectionDelegate`
routing capabilities. This also means that any `Router` implementation can be used directly to provide additional routing capabilities. This also means that any
as a ``request_callback`` for `~.httpserver.HTTPServer` constructor. `Router` implementation can be used directly as a ``request_callback``
for `~.httpserver.HTTPServer` constructor.
`Router` subclass must implement a ``find_handler`` method to provide a suitable `Router` subclass must implement a ``find_handler`` method to provide
`~.httputil.HTTPMessageDelegate` instance to handle the request: a suitable `~.httputil.HTTPMessageDelegate` instance to handle the
request:
.. code-block:: python .. code-block:: python
@ -44,16 +51,18 @@ as a ``request_callback`` for `~.httpserver.HTTPServer` constructor.
router = CustomRouter() router = CustomRouter()
server = HTTPServer(router) server = HTTPServer(router)
The main responsibility of `Router` implementation is to provide a mapping from a request The main responsibility of `Router` implementation is to provide a
to `~.httputil.HTTPMessageDelegate` instance that will handle this request. In the example above mapping from a request to `~.httputil.HTTPMessageDelegate` instance
we can see that routing is possible even without instantiating an `~.web.Application`. that will handle this request. In the example above we can see that
routing is possible even without instantiating an `~.web.Application`.
For routing to `~.web.RequestHandler` implementations we need an `~.web.Application` instance. For routing to `~.web.RequestHandler` implementations we need an
`~.web.Application.get_handler_delegate` provides a convenient way to create `~.web.Application` instance. `~.web.Application.get_handler_delegate`
`~.httputil.HTTPMessageDelegate` for a given request and `~.web.RequestHandler`. provides a convenient way to create `~.httputil.HTTPMessageDelegate`
for a given request and `~.web.RequestHandler`.
Here is a simple example of how we can we route to `~.web.RequestHandler` subclasses Here is a simple example of how we can we route to
by HTTP method: `~.web.RequestHandler` subclasses by HTTP method:
.. code-block:: python .. code-block:: python
@ -81,16 +90,18 @@ by HTTP method:
router = HTTPMethodRouter(Application()) router = HTTPMethodRouter(Application())
server = HTTPServer(router) server = HTTPServer(router)
`ReversibleRouter` interface adds the ability to distinguish between the routes and `ReversibleRouter` interface adds the ability to distinguish between
reverse them to the original urls using route's name and additional arguments. the routes and reverse them to the original urls using route's name
`~.web.Application` is itself an implementation of `ReversibleRouter` class. and additional arguments. `~.web.Application` is itself an
implementation of `ReversibleRouter` class.
`RuleRouter` and `ReversibleRuleRouter` are implementations of `Router` and `ReversibleRouter` `RuleRouter` and `ReversibleRuleRouter` are implementations of
interfaces and can be used for creating rule-based routing configurations. `Router` and `ReversibleRouter` interfaces and can be used for
creating rule-based routing configurations.
Rules are instances of `Rule` class. They contain a `Matcher`, which provides the logic for Rules are instances of `Rule` class. They contain a `Matcher`, which
determining whether the rule is a match for a particular request and a target, which can be provides the logic for determining whether the rule is a match for a
one of the following. particular request and a target, which can be one of the following.
1) An instance of `~.httputil.HTTPServerConnectionDelegate`: 1) An instance of `~.httputil.HTTPServerConnectionDelegate`:
@ -159,9 +170,12 @@ In the example below `RuleRouter` is used to route between applications:
server = HTTPServer(router) server = HTTPServer(router)
For more information on application-level routing see docs for `~.web.Application`. For more information on application-level routing see docs for `~.web.Application`.
.. versionadded:: 4.5
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import re import re
from functools import partial from functools import partial

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
from tornado.escape import utf8, _unicode from tornado.escape import utf8, _unicode
from tornado import gen from tornado import gen
@ -499,7 +499,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
def _should_follow_redirect(self): def _should_follow_redirect(self):
return (self.request.follow_redirects and return (self.request.follow_redirects and
self.request.max_redirects > 0 and self.request.max_redirects > 0 and
self.code in (301, 302, 303, 307)) self.code in (301, 302, 303, 307, 308))
def finish(self): def finish(self):
data = b''.join(self.chunks) data = b''.join(self.chunks)

View file

@ -67,7 +67,7 @@ Here are a few rules of thumb for when it's necessary:
block that references your `StackContext`. block that references your `StackContext`.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import sys import sys
import threading import threading
@ -82,6 +82,8 @@ class StackContextInconsistentError(Exception):
class _State(threading.local): class _State(threading.local):
def __init__(self): def __init__(self):
self.contexts = (tuple(), None) self.contexts = (tuple(), None)
_state = _State() _state = _State()

View file

@ -16,7 +16,7 @@
"""A non-blocking TCP connection factory. """A non-blocking TCP connection factory.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import functools import functools
import socket import socket
@ -155,16 +155,30 @@ class TCPClient(object):
@gen.coroutine @gen.coroutine
def connect(self, host, port, af=socket.AF_UNSPEC, ssl_options=None, def connect(self, host, port, af=socket.AF_UNSPEC, ssl_options=None,
max_buffer_size=None): max_buffer_size=None, source_ip=None, source_port=None):
"""Connect to the given host and port. """Connect to the given host and port.
Asynchronously returns an `.IOStream` (or `.SSLIOStream` if Asynchronously returns an `.IOStream` (or `.SSLIOStream` if
``ssl_options`` is not None). ``ssl_options`` is not None).
Using the ``source_ip`` kwarg, one can specify the source
IP address to use when establishing the connection.
In case the user needs to resolve and
use a specific interface, it has to be handled outside
of Tornado as this depends very much on the platform.
Similarly, when the user requires a certain source port, it can
be specified using the ``source_port`` arg.
.. versionchanged:: 4.5
Added the ``source_ip`` and ``source_port`` arguments.
""" """
addrinfo = yield self.resolver.resolve(host, port, af) addrinfo = yield self.resolver.resolve(host, port, af)
connector = _Connector( connector = _Connector(
addrinfo, self.io_loop, addrinfo, self.io_loop,
functools.partial(self._create_stream, max_buffer_size)) functools.partial(self._create_stream, max_buffer_size,
source_ip=source_ip, source_port=source_port)
)
af, addr, stream = yield connector.start() af, addr, stream = yield connector.start()
# TODO: For better performance we could cache the (af, addr) # TODO: For better performance we could cache the (af, addr)
# information here and re-use it on subsequent connections to # information here and re-use it on subsequent connections to
@ -174,13 +188,32 @@ class TCPClient(object):
server_hostname=host) server_hostname=host)
raise gen.Return(stream) raise gen.Return(stream)
def _create_stream(self, max_buffer_size, af, addr): def _create_stream(self, max_buffer_size, af, addr, source_ip=None,
source_port=None):
# Always connect in plaintext; we'll convert to ssl if necessary # Always connect in plaintext; we'll convert to ssl if necessary
# after one connection has completed. # after one connection has completed.
source_port_bind = source_port if isinstance(source_port, int) else 0
source_ip_bind = source_ip
if source_port_bind and not source_ip:
# User required a specific port, but did not specify
# a certain source IP, will bind to the default loopback.
source_ip_bind = '::1' if af == socket.AF_INET6 else '127.0.0.1'
# Trying to use the same address family as the requested af socket:
# - 127.0.0.1 for IPv4
# - ::1 for IPv6
socket_obj = socket.socket(af)
if source_port_bind or source_ip_bind:
# If the user requires binding also to a specific IP/port.
try:
socket_obj.bind((source_ip_bind, source_port_bind))
except socket.error:
socket_obj.close()
# Fail loudly if unable to use the IP/port.
raise
try: try:
stream = IOStream(socket.socket(af), stream = IOStream(socket_obj,
io_loop=self.io_loop, io_loop=self.io_loop,
max_buffer_size=max_buffer_size) max_buffer_size=max_buffer_size)
except socket.error as e: except socket.error as e:
fu = Future() fu = Future()
fu.set_exception(e) fu.set_exception(e)

View file

@ -15,7 +15,7 @@
# under the License. # under the License.
"""A non-blocking, single-threaded TCP server.""" """A non-blocking, single-threaded TCP server."""
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import errno import errno
import os import os

View file

@ -196,7 +196,7 @@ if you need to include a literal ``{{``, ``{%``, or ``{#`` in the output.
`filter_whitespace` for available options. New in Tornado 4.3. `filter_whitespace` for available options. New in Tornado 4.3.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import datetime import datetime
import linecache import linecache

View file

@ -10,7 +10,7 @@
for the tornado.autoreload module to rerun the tests when code changes. for the tornado.autoreload module to rerun the tests when code changes.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
try: try:
from tornado import gen from tornado import gen
@ -656,7 +656,9 @@ def main(**kwargs):
This test runner is essentially equivalent to `unittest.main` from This test runner is essentially equivalent to `unittest.main` from
the standard library, but adds support for tornado-style option the standard library, but adds support for tornado-style option
parsing and log formatting. parsing and log formatting. It is *not* necessary to use this
`main` function to run tests using `AsyncTestCase`; these tests
are self-contained and can run with any test runner.
The easiest way to run a test is via the command line:: The easiest way to run a test is via the command line::
@ -735,5 +737,6 @@ def main(**kwargs):
gen_log.error('FAIL') gen_log.error('FAIL')
raise raise
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -10,7 +10,7 @@ interface of its subclasses, including `.AsyncHTTPClient`, `.IOLoop`,
and `.Resolver`. and `.Resolver`.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import array import array
import atexit import atexit
@ -193,7 +193,11 @@ def exec_in(code, glob, loc=None):
if PY3: if PY3:
exec(""" exec("""
def raise_exc_info(exc_info): def raise_exc_info(exc_info):
raise exc_info[1].with_traceback(exc_info[2]) try:
raise exc_info[1].with_traceback(exc_info[2])
finally:
exc_info = None
""") """)
else: else:
exec(""" exec("""
@ -232,6 +236,7 @@ def _re_unescape_replacement(match):
raise ValueError("cannot unescape '\\\\%s'" % group[0]) raise ValueError("cannot unescape '\\\\%s'" % group[0])
return group return group
_re_unescape_pattern = re.compile(r'\\(.)', re.DOTALL) _re_unescape_pattern = re.compile(r'\\(.)', re.DOTALL)
@ -450,6 +455,7 @@ def _websocket_mask_python(mask, data):
else: else:
return unmasked_arr.tostring() return unmasked_arr.tostring()
if (os.environ.get('TORNADO_NO_EXTENSION') or if (os.environ.get('TORNADO_NO_EXTENSION') or
os.environ.get('TORNADO_EXTENSION') == '0'): os.environ.get('TORNADO_EXTENSION') == '0'):
# These environment variables exist to make it easier to do performance # These environment variables exist to make it easier to do performance

View file

@ -56,7 +56,7 @@ request.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import base64 import base64
import binascii import binascii
@ -756,45 +756,21 @@ class RequestHandler(object):
if body_part: if body_part:
html_bodies.append(utf8(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: if js_files:
# Maintain order of JavaScript files given by modules # Maintain order of JavaScript files given by modules
paths = [] js = self.render_linked_js(js_files)
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>') sloc = html.rindex(b'</body>')
html = html[:sloc] + utf8(js) + b'\n' + html[sloc:] html = html[:sloc] + utf8(js) + b'\n' + html[sloc:]
if js_embed: if js_embed:
js = b'<script type="text/javascript">\n//<![CDATA[\n' + \ js = self.render_embed_js(js_embed)
b'\n'.join(js_embed) + b'\n//]]>\n</script>'
sloc = html.rindex(b'</body>') sloc = html.rindex(b'</body>')
html = html[:sloc] + js + b'\n' + html[sloc:] html = html[:sloc] + js + b'\n' + html[sloc:]
if css_files: if css_files:
paths = [] css = self.render_linked_css(css_files)
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>') hloc = html.index(b'</head>')
html = html[:hloc] + utf8(css) + b'\n' + html[hloc:] html = html[:hloc] + utf8(css) + b'\n' + html[hloc:]
if css_embed: if css_embed:
css = b'<style type="text/css">\n' + b'\n'.join(css_embed) + \ css = self.render_embed_css(css_embed)
b'\n</style>'
hloc = html.index(b'</head>') hloc = html.index(b'</head>')
html = html[:hloc] + css + b'\n' + html[hloc:] html = html[:hloc] + css + b'\n' + html[hloc:]
if html_heads: if html_heads:
@ -805,6 +781,64 @@ class RequestHandler(object):
html = html[:hloc] + b''.join(html_bodies) + b'\n' + html[hloc:] html = html[:hloc] + b''.join(html_bodies) + b'\n' + html[hloc:]
self.finish(html) self.finish(html)
def render_linked_js(self, js_files):
"""Default method used to render the final js links for the
rendered webpage.
Override this method in a sub-classed controller to change the output.
"""
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)
return ''.join('<script src="' + escape.xhtml_escape(p) +
'" type="text/javascript"></script>'
for p in paths)
def render_embed_js(self, js_embed):
"""Default method used to render the final embedded js for the
rendered webpage.
Override this method in a sub-classed controller to change the output.
"""
return b'<script type="text/javascript">\n//<![CDATA[\n' + \
b'\n'.join(js_embed) + b'\n//]]>\n</script>'
def render_linked_css(self, css_files):
"""Default method used to render the final css links for the
rendered webpage.
Override this method in a sub-classed controller to change the output.
"""
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)
return ''.join('<link href="' + escape.xhtml_escape(p) + '" '
'type="text/css" rel="stylesheet"/>'
for p in paths)
def render_embed_css(self, css_embed):
"""Default method used to render the final embedded css for the
rendered webpage.
Override this method in a sub-classed controller to change the output.
"""
return b'<style type="text/css">\n' + b'\n'.join(css_embed) + \
b'\n</style>'
def render_string(self, template_name, **kwargs): def render_string(self, template_name, **kwargs):
"""Generate the given template with the given arguments. """Generate the given template with the given arguments.
@ -959,6 +993,9 @@ class RequestHandler(object):
self._log() self._log()
self._finished = True self._finished = True
self.on_finish() self.on_finish()
self._break_cycles()
def _break_cycles(self):
# Break up a reference cycle between this handler and the # Break up a reference cycle between this handler and the
# _ui_module closures to allow for faster GC on CPython. # _ui_module closures to allow for faster GC on CPython.
self.ui = None self.ui = None
@ -1672,10 +1709,6 @@ def stream_request_body(cls):
* The regular HTTP method (``post``, ``put``, etc) will be called after * The regular HTTP method (``post``, ``put``, etc) will be called after
the entire body has been read. the entire body has been read.
There is a subtle interaction between ``data_received`` and asynchronous
``prepare``: The first call to ``data_received`` may occur at any point
after the call to ``prepare`` has returned *or yielded*.
See the `file receiver demo <https://github.com/tornadoweb/tornado/tree/master/demos/file_upload/>`_ See the `file receiver demo <https://github.com/tornadoweb/tornado/tree/master/demos/file_upload/>`_
for example usage. for example usage.
""" """
@ -1834,6 +1867,8 @@ class Application(ReversibleRouter):
`StaticFileHandler` can be specified with the `StaticFileHandler` can be specified with the
``static_handler_class`` setting. ``static_handler_class`` setting.
.. versionchanged:: 4.5
Integration with the new `tornado.routing` module.
""" """
def __init__(self, handlers=None, default_host=None, transforms=None, def __init__(self, handlers=None, default_host=None, transforms=None,
**settings): **settings):
@ -2218,6 +2253,9 @@ class RedirectHandler(RequestHandler):
Use Python's :ref:`format string syntax <formatstrings>` to customize how Use Python's :ref:`format string syntax <formatstrings>` to customize how
values are substituted. values are substituted.
.. versionchanged:: 4.5
Added support for substitutions into the destination URL.
""" """
def initialize(self, url, permanent=True): def initialize(self, url, permanent=True):
self._url = url self._url = url
@ -3084,6 +3122,7 @@ def create_signed_value(secret, name, value, version=None, clock=None,
else: else:
raise ValueError("Unsupported version %d" % version) raise ValueError("Unsupported version %d" % version)
# A leading version number in decimal # A leading version number in decimal
# with no leading zeros, followed by a pipe. # with no leading zeros, followed by a pipe.
_signed_value_version_re = re.compile(br"^([1-9][0-9]*)\|(.*)$") _signed_value_version_re = re.compile(br"^([1-9][0-9]*)\|(.*)$")
@ -3240,3 +3279,7 @@ def _create_signature_v2(secret, s):
hash = hmac.new(utf8(secret), digestmod=hashlib.sha256) hash = hmac.new(utf8(secret), digestmod=hashlib.sha256)
hash.update(utf8(s)) hash.update(utf8(s))
return utf8(hash.hexdigest()) return utf8(hash.hexdigest())
def is_absolute(path):
return any(path.startswith(x) for x in ["/", "http:", "https:"])

View file

@ -16,7 +16,7 @@ the protocol (known as "draft 76") and are not compatible with this module.
Removed support for the draft 76 protocol version. Removed support for the draft 76 protocol version.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
# Author: Jacob Kristhammar, 2010 # Author: Jacob Kristhammar, 2010
import base64 import base64
@ -30,8 +30,8 @@ import zlib
from tornado.concurrent import TracebackFuture from tornado.concurrent import TracebackFuture
from tornado.escape import utf8, native_str, to_unicode from tornado.escape import utf8, native_str, to_unicode
from tornado import httpclient, httputil from tornado import gen, httpclient, httputil
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.iostream import StreamClosedError from tornado.iostream import StreamClosedError
from tornado.log import gen_log, app_log from tornado.log import gen_log, app_log
from tornado import simple_httpclient from tornado import simple_httpclient
@ -65,6 +65,10 @@ class WebSocketHandler(tornado.web.RequestHandler):
override `open` and `on_close` to handle opened and closed override `open` and `on_close` to handle opened and closed
connections. connections.
Custom upgrade response headers can be sent by overriding
`~tornado.web.RequestHandler.set_default_headers` or
`~tornado.web.RequestHandler.prepare`.
See http://dev.w3.org/html5/websockets/ for details on the See http://dev.w3.org/html5/websockets/ for details on the
JavaScript interface. The protocol is specified at JavaScript interface. The protocol is specified at
http://tools.ietf.org/html/rfc6455. http://tools.ietf.org/html/rfc6455.
@ -122,6 +126,17 @@ class WebSocketHandler(tornado.web.RequestHandler):
to show the "accept this certificate" dialog but has nowhere to show it. to show the "accept this certificate" dialog but has nowhere to show it.
You must first visit a regular HTML page using the same certificate You must first visit a regular HTML page using the same certificate
to accept it before the websocket connection will succeed. to accept it before the websocket connection will succeed.
If the application setting ``websocket_ping_interval`` has a non-zero
value, a ping will be sent periodically, and the connection will be
closed if a response is not received before the ``websocket_ping_timeout``.
Messages larger than the ``websocket_max_message_size`` application setting
(default 10MiB) will not be accepted.
.. versionchanged:: 4.5
Added ``websocket_ping_interval``, ``websocket_ping_timeout``, and
``websocket_max_message_size``.
""" """
def __init__(self, application, request, **kwargs): def __init__(self, application, request, **kwargs):
super(WebSocketHandler, self).__init__(application, request, **kwargs) super(WebSocketHandler, self).__init__(application, request, **kwargs)
@ -176,18 +191,42 @@ class WebSocketHandler(tornado.web.RequestHandler):
gen_log.debug(log_msg) gen_log.debug(log_msg)
return return
self.stream = self.request.connection.detach()
self.stream.set_close_callback(self.on_connection_close)
self.ws_connection = self.get_websocket_protocol() self.ws_connection = self.get_websocket_protocol()
if self.ws_connection: if self.ws_connection:
self.ws_connection.accept_connection() self.ws_connection.accept_connection()
else: else:
if not self.stream.closed(): self.set_status(426, "Upgrade Required")
self.stream.write(tornado.escape.utf8( self.set_header("Sec-WebSocket-Version", "7, 8, 13")
"HTTP/1.1 426 Upgrade Required\r\n" self.finish()
"Sec-WebSocket-Version: 7, 8, 13\r\n\r\n"))
self.stream.close() stream = None
@property
def ping_interval(self):
"""The interval for websocket keep-alive pings.
Set websocket_ping_interval = 0 to disable pings.
"""
return self.settings.get('websocket_ping_interval', None)
@property
def ping_timeout(self):
"""If no ping is received in this many seconds,
close the websocket connection (VPNs, etc. can fail to cleanly close ws connections).
Default is max of 3 pings or 30 seconds.
"""
return self.settings.get('websocket_ping_timeout', None)
@property
def max_message_size(self):
"""Maximum allowed message size.
If the remote peer sends a message larger than this, the connection
will be closed.
Default is 10MiB.
"""
return self.settings.get('websocket_max_message_size', None)
def write_message(self, message, binary=False): def write_message(self, message, binary=False):
"""Sends the given message to the client of this Web Socket. """Sends the given message to the client of this Web Socket.
@ -231,11 +270,22 @@ class WebSocketHandler(tornado.web.RequestHandler):
If this method returns None (the default), compression will If this method returns None (the default), compression will
be disabled. If it returns a dict (even an empty one), it be disabled. If it returns a dict (even an empty one), it
will be enabled. The contents of the dict may be used to will be enabled. The contents of the dict may be used to
control the memory and CPU usage of the compression, control the following compression options:
but no such options are currently implemented.
``compression_level`` specifies the compression level.
``mem_level`` specifies the amount of memory used for the internal compression state.
These parameters are documented in details here:
https://docs.python.org/3.6/library/zlib.html#zlib.compressobj
.. versionadded:: 4.1 .. versionadded:: 4.1
.. versionchanged:: 4.5
Added ``compression_level`` and ``mem_level``.
""" """
# TODO: Add wbits option.
return None return None
def open(self, *args, **kwargs): def open(self, *args, **kwargs):
@ -251,6 +301,10 @@ class WebSocketHandler(tornado.web.RequestHandler):
"""Handle incoming messages on the WebSocket """Handle incoming messages on the WebSocket
This method must be overridden. This method must be overridden.
.. versionchanged:: 4.5
``on_message`` can be a coroutine.
""" """
raise NotImplementedError raise NotImplementedError
@ -264,6 +318,10 @@ class WebSocketHandler(tornado.web.RequestHandler):
"""Invoked when the response to a ping frame is received.""" """Invoked when the response to a ping frame is received."""
pass pass
def on_ping(self, data):
"""Invoked when the a ping frame is received."""
pass
def on_close(self): def on_close(self):
"""Invoked when the WebSocket is closed. """Invoked when the WebSocket is closed.
@ -319,7 +377,7 @@ class WebSocketHandler(tornado.web.RequestHandler):
This is an important security measure; don't disable it This is an important security measure; don't disable it
without understanding the security implications. In without understanding the security implications. In
particular, if your authenticatino is cookie-based, you particular, if your authentication is cookie-based, you
must either restrict the origins allowed by must either restrict the origins allowed by
``check_origin()`` or implement your own XSRF-like ``check_origin()`` or implement your own XSRF-like
protection for websocket connections. See `these protection for websocket connections. See `these
@ -376,6 +434,16 @@ class WebSocketHandler(tornado.web.RequestHandler):
if not self._on_close_called: if not self._on_close_called:
self._on_close_called = True self._on_close_called = True
self.on_close() self.on_close()
self._break_cycles()
def _break_cycles(self):
# WebSocketHandlers call finish() early, but we don't want to
# break up reference cycles (which makes it impossible to call
# self.render_string) until after we've really closed the
# connection (if it was established in the first place,
# indicated by status code 101).
if self.get_status() != 101 or self._on_close_called:
super(WebSocketHandler, self)._break_cycles()
def send_error(self, *args, **kwargs): def send_error(self, *args, **kwargs):
if self.stream is None: if self.stream is None:
@ -393,18 +461,17 @@ class WebSocketHandler(tornado.web.RequestHandler):
return WebSocketProtocol13( return WebSocketProtocol13(
self, compression_options=self.get_compression_options()) self, compression_options=self.get_compression_options())
def _attach_stream(self):
self.stream = self.request.connection.detach()
self.stream.set_close_callback(self.on_connection_close)
# disable non-WS methods
for method in ["write", "redirect", "set_header", "set_cookie",
"set_status", "flush", "finish"]:
setattr(self, method, _raise_not_supported_for_websockets)
def _wrap_method(method):
def _disallow_for_websocket(self, *args, **kwargs): def _raise_not_supported_for_websockets(*args, **kwargs):
if self.stream is None: raise RuntimeError("Method not supported for Web Sockets")
method(self, *args, **kwargs)
else:
raise RuntimeError("Method not supported for Web Sockets")
return _disallow_for_websocket
for method in ["write", "redirect", "set_header", "set_cookie",
"set_status", "flush", "finish"]:
setattr(WebSocketHandler, method,
_wrap_method(getattr(WebSocketHandler, method)))
class WebSocketProtocol(object): class WebSocketProtocol(object):
@ -420,14 +487,20 @@ class WebSocketProtocol(object):
def _run_callback(self, callback, *args, **kwargs): def _run_callback(self, callback, *args, **kwargs):
"""Runs the given callback with exception handling. """Runs the given callback with exception handling.
On error, aborts the websocket connection and returns False. If the callback is a coroutine, returns its Future. On error, aborts the
websocket connection and returns None.
""" """
try: try:
callback(*args, **kwargs) result = callback(*args, **kwargs)
except Exception: except Exception:
app_log.error("Uncaught exception in %s", app_log.error("Uncaught exception in %s",
self.request.path, exc_info=True) getattr(self.request, 'path', None), exc_info=True)
self._abort() self._abort()
else:
if result is not None:
result = gen.convert_yielded(result)
self.stream.io_loop.add_future(result, lambda f: f.result())
return result
def on_connection_close(self): def on_connection_close(self):
self._abort() self._abort()
@ -441,7 +514,7 @@ class WebSocketProtocol(object):
class _PerMessageDeflateCompressor(object): class _PerMessageDeflateCompressor(object):
def __init__(self, persistent, max_wbits): def __init__(self, persistent, max_wbits, compression_options=None):
if max_wbits is None: if max_wbits is None:
max_wbits = zlib.MAX_WBITS max_wbits = zlib.MAX_WBITS
# There is no symbolic constant for the minimum wbits value. # There is no symbolic constant for the minimum wbits value.
@ -449,14 +522,24 @@ class _PerMessageDeflateCompressor(object):
raise ValueError("Invalid max_wbits value %r; allowed range 8-%d", raise ValueError("Invalid max_wbits value %r; allowed range 8-%d",
max_wbits, zlib.MAX_WBITS) max_wbits, zlib.MAX_WBITS)
self._max_wbits = max_wbits self._max_wbits = max_wbits
if compression_options is None or 'compression_level' not in compression_options:
self._compression_level = tornado.web.GZipContentEncoding.GZIP_LEVEL
else:
self._compression_level = compression_options['compression_level']
if compression_options is None or 'mem_level' not in compression_options:
self._mem_level = 8
else:
self._mem_level = compression_options['mem_level']
if persistent: if persistent:
self._compressor = self._create_compressor() self._compressor = self._create_compressor()
else: else:
self._compressor = None self._compressor = None
def _create_compressor(self): def _create_compressor(self):
return zlib.compressobj(tornado.web.GZipContentEncoding.GZIP_LEVEL, return zlib.compressobj(self._compression_level, zlib.DEFLATED, -self._max_wbits, self._mem_level)
zlib.DEFLATED, -self._max_wbits)
def compress(self, data): def compress(self, data):
compressor = self._compressor or self._create_compressor() compressor = self._compressor or self._create_compressor()
@ -467,7 +550,7 @@ class _PerMessageDeflateCompressor(object):
class _PerMessageDeflateDecompressor(object): class _PerMessageDeflateDecompressor(object):
def __init__(self, persistent, max_wbits): def __init__(self, persistent, max_wbits, compression_options=None):
if max_wbits is None: if max_wbits is None:
max_wbits = zlib.MAX_WBITS max_wbits = zlib.MAX_WBITS
if not (8 <= max_wbits <= zlib.MAX_WBITS): if not (8 <= max_wbits <= zlib.MAX_WBITS):
@ -526,6 +609,9 @@ class WebSocketProtocol13(WebSocketProtocol):
# the effect of compression, frame overhead, and control frames. # the effect of compression, frame overhead, and control frames.
self._wire_bytes_in = 0 self._wire_bytes_in = 0
self._wire_bytes_out = 0 self._wire_bytes_out = 0
self.ping_callback = None
self.last_ping = 0
self.last_pong = 0
def accept_connection(self): def accept_connection(self):
try: try:
@ -562,46 +648,42 @@ class WebSocketProtocol13(WebSocketProtocol):
self.request.headers.get("Sec-Websocket-Key")) self.request.headers.get("Sec-Websocket-Key"))
def _accept_connection(self): def _accept_connection(self):
subprotocol_header = ''
subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '') subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '')
subprotocols = [s.strip() for s in subprotocols.split(',')] subprotocols = [s.strip() for s in subprotocols.split(',')]
if subprotocols: if subprotocols:
selected = self.handler.select_subprotocol(subprotocols) selected = self.handler.select_subprotocol(subprotocols)
if selected: if selected:
assert selected in subprotocols assert selected in subprotocols
subprotocol_header = ("Sec-WebSocket-Protocol: %s\r\n" self.handler.set_header("Sec-WebSocket-Protocol", selected)
% selected)
extension_header = ''
extensions = self._parse_extensions_header(self.request.headers) extensions = self._parse_extensions_header(self.request.headers)
for ext in extensions: for ext in extensions:
if (ext[0] == 'permessage-deflate' and if (ext[0] == 'permessage-deflate' and
self._compression_options is not None): self._compression_options is not None):
# TODO: negotiate parameters if compression_options # TODO: negotiate parameters if compression_options
# specifies limits. # specifies limits.
self._create_compressors('server', ext[1]) self._create_compressors('server', ext[1], self._compression_options)
if ('client_max_window_bits' in ext[1] and if ('client_max_window_bits' in ext[1] and
ext[1]['client_max_window_bits'] is None): ext[1]['client_max_window_bits'] is None):
# Don't echo an offered client_max_window_bits # Don't echo an offered client_max_window_bits
# parameter with no value. # parameter with no value.
del ext[1]['client_max_window_bits'] del ext[1]['client_max_window_bits']
extension_header = ('Sec-WebSocket-Extensions: %s\r\n' % self.handler.set_header("Sec-WebSocket-Extensions",
httputil._encode_header( httputil._encode_header(
'permessage-deflate', ext[1])) 'permessage-deflate', ext[1]))
break break
if self.stream.closed(): self.handler.clear_header("Content-Type")
self._abort() self.handler.set_status(101)
return self.handler.set_header("Upgrade", "websocket")
self.stream.write(tornado.escape.utf8( self.handler.set_header("Connection", "Upgrade")
"HTTP/1.1 101 Switching Protocols\r\n" self.handler.set_header("Sec-WebSocket-Accept", self._challenge_response())
"Upgrade: websocket\r\n" self.handler.finish()
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n"
"%s%s"
"\r\n" % (self._challenge_response(),
subprotocol_header, extension_header)))
self.handler._attach_stream()
self.stream = self.handler.stream
self.start_pinging()
self._run_callback(self.handler.open, *self.handler.open_args, self._run_callback(self.handler.open, *self.handler.open_args,
**self.handler.open_kwargs) **self.handler.open_kwargs)
self._receive_frame() self._receive_frame()
@ -631,7 +713,7 @@ class WebSocketProtocol13(WebSocketProtocol):
else: else:
raise ValueError("unsupported extension %r", ext) raise ValueError("unsupported extension %r", ext)
def _get_compressor_options(self, side, agreed_parameters): def _get_compressor_options(self, side, agreed_parameters, compression_options=None):
"""Converts a websocket agreed_parameters set to keyword arguments """Converts a websocket agreed_parameters set to keyword arguments
for our compressor objects. for our compressor objects.
""" """
@ -642,9 +724,10 @@ class WebSocketProtocol13(WebSocketProtocol):
options['max_wbits'] = zlib.MAX_WBITS options['max_wbits'] = zlib.MAX_WBITS
else: else:
options['max_wbits'] = int(wbits_header) options['max_wbits'] = int(wbits_header)
options['compression_options'] = compression_options
return options return options
def _create_compressors(self, side, agreed_parameters): def _create_compressors(self, side, agreed_parameters, compression_options=None):
# TODO: handle invalid parameters gracefully # TODO: handle invalid parameters gracefully
allowed_keys = set(['server_no_context_takeover', allowed_keys = set(['server_no_context_takeover',
'client_no_context_takeover', 'client_no_context_takeover',
@ -655,9 +738,9 @@ class WebSocketProtocol13(WebSocketProtocol):
raise ValueError("unsupported compression parameter %r" % key) raise ValueError("unsupported compression parameter %r" % key)
other_side = 'client' if (side == 'server') else 'server' other_side = 'client' if (side == 'server') else 'server'
self._compressor = _PerMessageDeflateCompressor( self._compressor = _PerMessageDeflateCompressor(
**self._get_compressor_options(side, agreed_parameters)) **self._get_compressor_options(side, agreed_parameters, compression_options))
self._decompressor = _PerMessageDeflateDecompressor( self._decompressor = _PerMessageDeflateDecompressor(
**self._get_compressor_options(other_side, agreed_parameters)) **self._get_compressor_options(other_side, agreed_parameters, compression_options))
def _write_frame(self, fin, opcode, data, flags=0): def _write_frame(self, fin, opcode, data, flags=0):
if fin: if fin:
@ -738,8 +821,7 @@ class WebSocketProtocol13(WebSocketProtocol):
if self._masked_frame: if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key) self.stream.read_bytes(4, self._on_masking_key)
else: else:
self.stream.read_bytes(self._frame_length, self._read_frame_data(False)
self._on_frame_data)
elif payloadlen == 126: elif payloadlen == 126:
self.stream.read_bytes(2, self._on_frame_length_16) self.stream.read_bytes(2, self._on_frame_length_16)
elif payloadlen == 127: elif payloadlen == 127:
@ -747,6 +829,17 @@ class WebSocketProtocol13(WebSocketProtocol):
except StreamClosedError: except StreamClosedError:
self._abort() self._abort()
def _read_frame_data(self, masked):
new_len = self._frame_length
if self._fragmented_message_buffer is not None:
new_len += len(self._fragmented_message_buffer)
if new_len > (self.handler.max_message_size or 10 * 1024 * 1024):
self.close(1009, "message too big")
return
self.stream.read_bytes(
self._frame_length,
self._on_masked_frame_data if masked else self._on_frame_data)
def _on_frame_length_16(self, data): def _on_frame_length_16(self, data):
self._wire_bytes_in += len(data) self._wire_bytes_in += len(data)
self._frame_length = struct.unpack("!H", data)[0] self._frame_length = struct.unpack("!H", data)[0]
@ -754,7 +847,7 @@ class WebSocketProtocol13(WebSocketProtocol):
if self._masked_frame: if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key) self.stream.read_bytes(4, self._on_masking_key)
else: else:
self.stream.read_bytes(self._frame_length, self._on_frame_data) self._read_frame_data(False)
except StreamClosedError: except StreamClosedError:
self._abort() self._abort()
@ -765,7 +858,7 @@ class WebSocketProtocol13(WebSocketProtocol):
if self._masked_frame: if self._masked_frame:
self.stream.read_bytes(4, self._on_masking_key) self.stream.read_bytes(4, self._on_masking_key)
else: else:
self.stream.read_bytes(self._frame_length, self._on_frame_data) self._read_frame_data(False)
except StreamClosedError: except StreamClosedError:
self._abort() self._abort()
@ -773,8 +866,7 @@ class WebSocketProtocol13(WebSocketProtocol):
self._wire_bytes_in += len(data) self._wire_bytes_in += len(data)
self._frame_mask = data self._frame_mask = data
try: try:
self.stream.read_bytes(self._frame_length, self._read_frame_data(True)
self._on_masked_frame_data)
except StreamClosedError: except StreamClosedError:
self._abort() self._abort()
@ -783,6 +875,8 @@ class WebSocketProtocol13(WebSocketProtocol):
self._on_frame_data(_websocket_mask(self._frame_mask, data)) self._on_frame_data(_websocket_mask(self._frame_mask, data))
def _on_frame_data(self, data): def _on_frame_data(self, data):
handled_future = None
self._wire_bytes_in += len(data) self._wire_bytes_in += len(data)
if self._frame_opcode_is_control: if self._frame_opcode_is_control:
# control frames may be interleaved with a series of fragmented # control frames may be interleaved with a series of fragmented
@ -815,12 +909,18 @@ class WebSocketProtocol13(WebSocketProtocol):
self._fragmented_message_buffer = data self._fragmented_message_buffer = data
if self._final_frame: if self._final_frame:
self._handle_message(opcode, data) handled_future = self._handle_message(opcode, data)
if not self.client_terminated: if not self.client_terminated:
self._receive_frame() if handled_future:
# on_message is a coroutine, process more frames once it's done.
handled_future.add_done_callback(
lambda future: self._receive_frame())
else:
self._receive_frame()
def _handle_message(self, opcode, data): def _handle_message(self, opcode, data):
"""Execute on_message, returning its Future if it is a coroutine."""
if self.client_terminated: if self.client_terminated:
return return
@ -835,11 +935,11 @@ class WebSocketProtocol13(WebSocketProtocol):
except UnicodeDecodeError: except UnicodeDecodeError:
self._abort() self._abort()
return return
self._run_callback(self.handler.on_message, decoded) return self._run_callback(self.handler.on_message, decoded)
elif opcode == 0x2: elif opcode == 0x2:
# Binary data # Binary data
self._message_bytes_in += len(data) self._message_bytes_in += len(data)
self._run_callback(self.handler.on_message, data) return self._run_callback(self.handler.on_message, data)
elif opcode == 0x8: elif opcode == 0x8:
# Close # Close
self.client_terminated = True self.client_terminated = True
@ -852,9 +952,11 @@ class WebSocketProtocol13(WebSocketProtocol):
elif opcode == 0x9: elif opcode == 0x9:
# Ping # Ping
self._write_frame(True, 0xA, data) self._write_frame(True, 0xA, data)
self._run_callback(self.handler.on_ping, data)
elif opcode == 0xA: elif opcode == 0xA:
# Pong # Pong
self._run_callback(self.handler.on_pong, data) self.last_pong = IOLoop.current().time()
return self._run_callback(self.handler.on_pong, data)
else: else:
self._abort() self._abort()
@ -883,6 +985,51 @@ class WebSocketProtocol13(WebSocketProtocol):
self._waiting = self.stream.io_loop.add_timeout( self._waiting = self.stream.io_loop.add_timeout(
self.stream.io_loop.time() + 5, self._abort) self.stream.io_loop.time() + 5, self._abort)
@property
def ping_interval(self):
interval = self.handler.ping_interval
if interval is not None:
return interval
return 0
@property
def ping_timeout(self):
timeout = self.handler.ping_timeout
if timeout is not None:
return timeout
return max(3 * self.ping_interval, 30)
def start_pinging(self):
"""Start sending periodic pings to keep the connection alive"""
if self.ping_interval > 0:
self.last_ping = self.last_pong = IOLoop.current().time()
self.ping_callback = PeriodicCallback(
self.periodic_ping, self.ping_interval * 1000)
self.ping_callback.start()
def periodic_ping(self):
"""Send a ping to keep the websocket alive
Called periodically if the websocket_ping_interval is set and non-zero.
"""
if self.stream.closed() and self.ping_callback is not None:
self.ping_callback.stop()
return
# Check for timeout on pong. Make sure that we really have
# sent a recent ping in case the machine with both server and
# client has been suspended since the last ping.
now = IOLoop.current().time()
since_last_pong = now - self.last_pong
since_last_ping = now - self.last_ping
if (since_last_ping < 2 * self.ping_interval and
since_last_pong > self.ping_timeout):
self.close()
return
self.write_ping(b'')
self.last_ping = now
class WebSocketClientConnection(simple_httpclient._HTTPConnection): class WebSocketClientConnection(simple_httpclient._HTTPConnection):
"""WebSocket client connection. """WebSocket client connection.
@ -891,7 +1038,8 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
`websocket_connect` function instead. `websocket_connect` function instead.
""" """
def __init__(self, io_loop, request, on_message_callback=None, def __init__(self, io_loop, request, on_message_callback=None,
compression_options=None): compression_options=None, ping_interval=None, ping_timeout=None,
max_message_size=None):
self.compression_options = compression_options self.compression_options = compression_options
self.connect_future = TracebackFuture() self.connect_future = TracebackFuture()
self.protocol = None self.protocol = None
@ -900,6 +1048,9 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
self.key = base64.b64encode(os.urandom(16)) self.key = base64.b64encode(os.urandom(16))
self._on_message_callback = on_message_callback self._on_message_callback = on_message_callback
self.close_code = self.close_reason = None self.close_code = self.close_reason = None
self.ping_interval = ping_interval
self.ping_timeout = ping_timeout
self.max_message_size = max_message_size
scheme, sep, rest = request.url.partition(':') scheme, sep, rest = request.url.partition(':')
scheme = {'ws': 'http', 'wss': 'https'}[scheme] scheme = {'ws': 'http', 'wss': 'https'}[scheme]
@ -963,6 +1114,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
self.headers = headers self.headers = headers
self.protocol = self.get_websocket_protocol() self.protocol = self.get_websocket_protocol()
self.protocol._process_server_headers(self.key, self.headers) self.protocol._process_server_headers(self.key, self.headers)
self.protocol.start_pinging()
self.protocol._receive_frame() self.protocol._receive_frame()
if self._timeout is not None: if self._timeout is not None:
@ -1016,13 +1168,18 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection):
def on_pong(self, data): def on_pong(self, data):
pass pass
def on_ping(self, data):
pass
def get_websocket_protocol(self): def get_websocket_protocol(self):
return WebSocketProtocol13(self, mask_outgoing=True, return WebSocketProtocol13(self, mask_outgoing=True,
compression_options=self.compression_options) compression_options=self.compression_options)
def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None, def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None,
on_message_callback=None, compression_options=None): on_message_callback=None, compression_options=None,
ping_interval=None, ping_timeout=None,
max_message_size=None):
"""Client-side websocket support. """Client-side websocket support.
Takes a url and returns a Future whose result is a Takes a url and returns a Future whose result is a
@ -1051,6 +1208,10 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None,
.. versionchanged:: 4.1 .. versionchanged:: 4.1
Added ``compression_options`` and ``on_message_callback``. Added ``compression_options`` and ``on_message_callback``.
The ``io_loop`` argument is deprecated. The ``io_loop`` argument is deprecated.
.. versionchanged:: 4.5
Added the ``ping_interval``, ``ping_timeout``, and ``max_message_size``
arguments, which have the same meaning as in `WebSocketHandler`.
""" """
if io_loop is None: if io_loop is None:
io_loop = IOLoop.current() io_loop = IOLoop.current()
@ -1066,7 +1227,10 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None,
request, httpclient.HTTPRequest._DEFAULTS) request, httpclient.HTTPRequest._DEFAULTS)
conn = WebSocketClientConnection(io_loop, request, conn = WebSocketClientConnection(io_loop, request,
on_message_callback=on_message_callback, on_message_callback=on_message_callback,
compression_options=compression_options) compression_options=compression_options,
ping_interval=ping_interval,
ping_timeout=ping_timeout,
max_message_size=max_message_size)
if callback is not None: if callback is not None:
io_loop.add_future(conn.connect_future, callback) io_loop.add_future(conn.connect_future, callback)
return conn.connect_future return conn.connect_future

View file

@ -29,7 +29,7 @@ provides WSGI support in two ways:
and Tornado handlers in a single server. and Tornado handlers in a single server.
""" """
from __future__ import absolute_import, division, print_function, with_statement from __future__ import absolute_import, division, print_function
import sys import sys
from io import BytesIO from io import BytesIO