|
|
@ -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. |
|
|
|
""" |
|
|
|
|
|
|
|
from __future__ import absolute_import, division, print_function, with_statement |
|
|
|
from __future__ import absolute_import, division, print_function |
|
|
|
# Author: Jacob Kristhammar, 2010 |
|
|
|
|
|
|
|
import base64 |
|
|
@ -30,8 +30,8 @@ import zlib |
|
|
|
|
|
|
|
from tornado.concurrent import TracebackFuture |
|
|
|
from tornado.escape import utf8, native_str, to_unicode |
|
|
|
from tornado import httpclient, httputil |
|
|
|
from tornado.ioloop import IOLoop |
|
|
|
from tornado import gen, httpclient, httputil |
|
|
|
from tornado.ioloop import IOLoop, PeriodicCallback |
|
|
|
from tornado.iostream import StreamClosedError |
|
|
|
from tornado.log import gen_log, app_log |
|
|
|
from tornado import simple_httpclient |
|
|
@ -65,6 +65,10 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
override `open` and `on_close` to handle opened and closed |
|
|
|
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 |
|
|
|
JavaScript interface. The protocol is specified at |
|
|
|
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. |
|
|
|
You must first visit a regular HTML page using the same certificate |
|
|
|
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): |
|
|
|
super(WebSocketHandler, self).__init__(application, request, **kwargs) |
|
|
@ -176,18 +191,42 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
gen_log.debug(log_msg) |
|
|
|
return |
|
|
|
|
|
|
|
self.stream = self.request.connection.detach() |
|
|
|
self.stream.set_close_callback(self.on_connection_close) |
|
|
|
|
|
|
|
self.ws_connection = self.get_websocket_protocol() |
|
|
|
if self.ws_connection: |
|
|
|
self.ws_connection.accept_connection() |
|
|
|
else: |
|
|
|
if not self.stream.closed(): |
|
|
|
self.stream.write(tornado.escape.utf8( |
|
|
|
"HTTP/1.1 426 Upgrade Required\r\n" |
|
|
|
"Sec-WebSocket-Version: 7, 8, 13\r\n\r\n")) |
|
|
|
self.stream.close() |
|
|
|
self.set_status(426, "Upgrade Required") |
|
|
|
self.set_header("Sec-WebSocket-Version", "7, 8, 13") |
|
|
|
self.finish() |
|
|
|
|
|
|
|
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): |
|
|
|
"""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 |
|
|
|
be disabled. If it returns a dict (even an empty one), it |
|
|
|
will be enabled. The contents of the dict may be used to |
|
|
|
control the memory and CPU usage of the compression, |
|
|
|
but no such options are currently implemented. |
|
|
|
control the following compression options: |
|
|
|
|
|
|
|
``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 |
|
|
|
|
|
|
|
.. versionchanged:: 4.5 |
|
|
|
|
|
|
|
Added ``compression_level`` and ``mem_level``. |
|
|
|
""" |
|
|
|
# TODO: Add wbits option. |
|
|
|
return None |
|
|
|
|
|
|
|
def open(self, *args, **kwargs): |
|
|
@ -251,6 +301,10 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
"""Handle incoming messages on the WebSocket |
|
|
|
|
|
|
|
This method must be overridden. |
|
|
|
|
|
|
|
.. versionchanged:: 4.5 |
|
|
|
|
|
|
|
``on_message`` can be a coroutine. |
|
|
|
""" |
|
|
|
raise NotImplementedError |
|
|
|
|
|
|
@ -264,6 +318,10 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
"""Invoked when the response to a ping frame is received.""" |
|
|
|
pass |
|
|
|
|
|
|
|
def on_ping(self, data): |
|
|
|
"""Invoked when the a ping frame is received.""" |
|
|
|
pass |
|
|
|
|
|
|
|
def on_close(self): |
|
|
|
"""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 |
|
|
|
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 |
|
|
|
``check_origin()`` or implement your own XSRF-like |
|
|
|
protection for websocket connections. See `these |
|
|
@ -376,6 +434,16 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
if not self._on_close_called: |
|
|
|
self._on_close_called = True |
|
|
|
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): |
|
|
|
if self.stream is None: |
|
|
@ -393,18 +461,17 @@ class WebSocketHandler(tornado.web.RequestHandler): |
|
|
|
return WebSocketProtocol13( |
|
|
|
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): |
|
|
|
if self.stream is None: |
|
|
|
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))) |
|
|
|
|
|
|
|
def _raise_not_supported_for_websockets(*args, **kwargs): |
|
|
|
raise RuntimeError("Method not supported for Web Sockets") |
|
|
|
|
|
|
|
|
|
|
|
class WebSocketProtocol(object): |
|
|
@ -420,14 +487,20 @@ class WebSocketProtocol(object): |
|
|
|
def _run_callback(self, callback, *args, **kwargs): |
|
|
|
"""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: |
|
|
|
callback(*args, **kwargs) |
|
|
|
result = callback(*args, **kwargs) |
|
|
|
except Exception: |
|
|
|
app_log.error("Uncaught exception in %s", |
|
|
|
self.request.path, exc_info=True) |
|
|
|
getattr(self.request, 'path', None), exc_info=True) |
|
|
|
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): |
|
|
|
self._abort() |
|
|
@ -441,7 +514,7 @@ class WebSocketProtocol(object): |
|
|
|
|
|
|
|
|
|
|
|
class _PerMessageDeflateCompressor(object): |
|
|
|
def __init__(self, persistent, max_wbits): |
|
|
|
def __init__(self, persistent, max_wbits, compression_options=None): |
|
|
|
if max_wbits is None: |
|
|
|
max_wbits = zlib.MAX_WBITS |
|
|
|
# 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", |
|
|
|
max_wbits, zlib.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: |
|
|
|
self._compressor = self._create_compressor() |
|
|
|
else: |
|
|
|
self._compressor = None |
|
|
|
|
|
|
|
def _create_compressor(self): |
|
|
|
return zlib.compressobj(tornado.web.GZipContentEncoding.GZIP_LEVEL, |
|
|
|
zlib.DEFLATED, -self._max_wbits) |
|
|
|
return zlib.compressobj(self._compression_level, zlib.DEFLATED, -self._max_wbits, self._mem_level) |
|
|
|
|
|
|
|
def compress(self, data): |
|
|
|
compressor = self._compressor or self._create_compressor() |
|
|
@ -467,7 +550,7 @@ class _PerMessageDeflateCompressor(object): |
|
|
|
|
|
|
|
|
|
|
|
class _PerMessageDeflateDecompressor(object): |
|
|
|
def __init__(self, persistent, max_wbits): |
|
|
|
def __init__(self, persistent, max_wbits, compression_options=None): |
|
|
|
if max_wbits is None: |
|
|
|
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. |
|
|
|
self._wire_bytes_in = 0 |
|
|
|
self._wire_bytes_out = 0 |
|
|
|
self.ping_callback = None |
|
|
|
self.last_ping = 0 |
|
|
|
self.last_pong = 0 |
|
|
|
|
|
|
|
def accept_connection(self): |
|
|
|
try: |
|
|
@ -562,46 +648,42 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
self.request.headers.get("Sec-Websocket-Key")) |
|
|
|
|
|
|
|
def _accept_connection(self): |
|
|
|
subprotocol_header = '' |
|
|
|
subprotocols = self.request.headers.get("Sec-WebSocket-Protocol", '') |
|
|
|
subprotocols = [s.strip() for s in subprotocols.split(',')] |
|
|
|
if subprotocols: |
|
|
|
selected = self.handler.select_subprotocol(subprotocols) |
|
|
|
if selected: |
|
|
|
assert selected in subprotocols |
|
|
|
subprotocol_header = ("Sec-WebSocket-Protocol: %s\r\n" |
|
|
|
% selected) |
|
|
|
self.handler.set_header("Sec-WebSocket-Protocol", selected) |
|
|
|
|
|
|
|
extension_header = '' |
|
|
|
extensions = self._parse_extensions_header(self.request.headers) |
|
|
|
for ext in extensions: |
|
|
|
if (ext[0] == 'permessage-deflate' and |
|
|
|
self._compression_options is not None): |
|
|
|
# TODO: negotiate parameters if compression_options |
|
|
|
# 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 |
|
|
|
ext[1]['client_max_window_bits'] is None): |
|
|
|
# Don't echo an offered client_max_window_bits |
|
|
|
# parameter with no value. |
|
|
|
del ext[1]['client_max_window_bits'] |
|
|
|
extension_header = ('Sec-WebSocket-Extensions: %s\r\n' % |
|
|
|
httputil._encode_header( |
|
|
|
'permessage-deflate', ext[1])) |
|
|
|
self.handler.set_header("Sec-WebSocket-Extensions", |
|
|
|
httputil._encode_header( |
|
|
|
'permessage-deflate', ext[1])) |
|
|
|
break |
|
|
|
|
|
|
|
if self.stream.closed(): |
|
|
|
self._abort() |
|
|
|
return |
|
|
|
self.stream.write(tornado.escape.utf8( |
|
|
|
"HTTP/1.1 101 Switching Protocols\r\n" |
|
|
|
"Upgrade: websocket\r\n" |
|
|
|
"Connection: Upgrade\r\n" |
|
|
|
"Sec-WebSocket-Accept: %s\r\n" |
|
|
|
"%s%s" |
|
|
|
"\r\n" % (self._challenge_response(), |
|
|
|
subprotocol_header, extension_header))) |
|
|
|
self.handler.clear_header("Content-Type") |
|
|
|
self.handler.set_status(101) |
|
|
|
self.handler.set_header("Upgrade", "websocket") |
|
|
|
self.handler.set_header("Connection", "Upgrade") |
|
|
|
self.handler.set_header("Sec-WebSocket-Accept", self._challenge_response()) |
|
|
|
self.handler.finish() |
|
|
|
|
|
|
|
self.handler._attach_stream() |
|
|
|
self.stream = self.handler.stream |
|
|
|
|
|
|
|
self.start_pinging() |
|
|
|
self._run_callback(self.handler.open, *self.handler.open_args, |
|
|
|
**self.handler.open_kwargs) |
|
|
|
self._receive_frame() |
|
|
@ -631,7 +713,7 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
else: |
|
|
|
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 |
|
|
|
for our compressor objects. |
|
|
|
""" |
|
|
@ -642,9 +724,10 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
options['max_wbits'] = zlib.MAX_WBITS |
|
|
|
else: |
|
|
|
options['max_wbits'] = int(wbits_header) |
|
|
|
options['compression_options'] = compression_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 |
|
|
|
allowed_keys = set(['server_no_context_takeover', |
|
|
|
'client_no_context_takeover', |
|
|
@ -655,9 +738,9 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
raise ValueError("unsupported compression parameter %r" % key) |
|
|
|
other_side = 'client' if (side == 'server') else 'server' |
|
|
|
self._compressor = _PerMessageDeflateCompressor( |
|
|
|
**self._get_compressor_options(side, agreed_parameters)) |
|
|
|
**self._get_compressor_options(side, agreed_parameters, compression_options)) |
|
|
|
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): |
|
|
|
if fin: |
|
|
@ -738,8 +821,7 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
if self._masked_frame: |
|
|
|
self.stream.read_bytes(4, self._on_masking_key) |
|
|
|
else: |
|
|
|
self.stream.read_bytes(self._frame_length, |
|
|
|
self._on_frame_data) |
|
|
|
self._read_frame_data(False) |
|
|
|
elif payloadlen == 126: |
|
|
|
self.stream.read_bytes(2, self._on_frame_length_16) |
|
|
|
elif payloadlen == 127: |
|
|
@ -747,6 +829,17 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
except StreamClosedError: |
|
|
|
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): |
|
|
|
self._wire_bytes_in += len(data) |
|
|
|
self._frame_length = struct.unpack("!H", data)[0] |
|
|
@ -754,7 +847,7 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
if self._masked_frame: |
|
|
|
self.stream.read_bytes(4, self._on_masking_key) |
|
|
|
else: |
|
|
|
self.stream.read_bytes(self._frame_length, self._on_frame_data) |
|
|
|
self._read_frame_data(False) |
|
|
|
except StreamClosedError: |
|
|
|
self._abort() |
|
|
|
|
|
|
@ -765,7 +858,7 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
if self._masked_frame: |
|
|
|
self.stream.read_bytes(4, self._on_masking_key) |
|
|
|
else: |
|
|
|
self.stream.read_bytes(self._frame_length, self._on_frame_data) |
|
|
|
self._read_frame_data(False) |
|
|
|
except StreamClosedError: |
|
|
|
self._abort() |
|
|
|
|
|
|
@ -773,8 +866,7 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
self._wire_bytes_in += len(data) |
|
|
|
self._frame_mask = data |
|
|
|
try: |
|
|
|
self.stream.read_bytes(self._frame_length, |
|
|
|
self._on_masked_frame_data) |
|
|
|
self._read_frame_data(True) |
|
|
|
except StreamClosedError: |
|
|
|
self._abort() |
|
|
|
|
|
|
@ -783,6 +875,8 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
self._on_frame_data(_websocket_mask(self._frame_mask, data)) |
|
|
|
|
|
|
|
def _on_frame_data(self, data): |
|
|
|
handled_future = None |
|
|
|
|
|
|
|
self._wire_bytes_in += len(data) |
|
|
|
if self._frame_opcode_is_control: |
|
|
|
# control frames may be interleaved with a series of fragmented |
|
|
@ -815,12 +909,18 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
self._fragmented_message_buffer = data |
|
|
|
|
|
|
|
if self._final_frame: |
|
|
|
self._handle_message(opcode, data) |
|
|
|
handled_future = self._handle_message(opcode, data) |
|
|
|
|
|
|
|
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): |
|
|
|
"""Execute on_message, returning its Future if it is a coroutine.""" |
|
|
|
if self.client_terminated: |
|
|
|
return |
|
|
|
|
|
|
@ -835,11 +935,11 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
except UnicodeDecodeError: |
|
|
|
self._abort() |
|
|
|
return |
|
|
|
self._run_callback(self.handler.on_message, decoded) |
|
|
|
return self._run_callback(self.handler.on_message, decoded) |
|
|
|
elif opcode == 0x2: |
|
|
|
# Binary 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: |
|
|
|
# Close |
|
|
|
self.client_terminated = True |
|
|
@ -852,9 +952,11 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
elif opcode == 0x9: |
|
|
|
# Ping |
|
|
|
self._write_frame(True, 0xA, data) |
|
|
|
self._run_callback(self.handler.on_ping, data) |
|
|
|
elif opcode == 0xA: |
|
|
|
# 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: |
|
|
|
self._abort() |
|
|
|
|
|
|
@ -883,6 +985,51 @@ class WebSocketProtocol13(WebSocketProtocol): |
|
|
|
self._waiting = self.stream.io_loop.add_timeout( |
|
|
|
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): |
|
|
|
"""WebSocket client connection. |
|
|
@ -891,7 +1038,8 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): |
|
|
|
`websocket_connect` function instead. |
|
|
|
""" |
|
|
|
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.connect_future = TracebackFuture() |
|
|
|
self.protocol = None |
|
|
@ -900,6 +1048,9 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): |
|
|
|
self.key = base64.b64encode(os.urandom(16)) |
|
|
|
self._on_message_callback = on_message_callback |
|
|
|
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 = {'ws': 'http', 'wss': 'https'}[scheme] |
|
|
@ -963,6 +1114,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): |
|
|
|
self.headers = headers |
|
|
|
self.protocol = self.get_websocket_protocol() |
|
|
|
self.protocol._process_server_headers(self.key, self.headers) |
|
|
|
self.protocol.start_pinging() |
|
|
|
self.protocol._receive_frame() |
|
|
|
|
|
|
|
if self._timeout is not None: |
|
|
@ -1016,13 +1168,18 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): |
|
|
|
def on_pong(self, data): |
|
|
|
pass |
|
|
|
|
|
|
|
def on_ping(self, data): |
|
|
|
pass |
|
|
|
|
|
|
|
def get_websocket_protocol(self): |
|
|
|
return WebSocketProtocol13(self, mask_outgoing=True, |
|
|
|
compression_options=self.compression_options) |
|
|
|
|
|
|
|
|
|
|
|
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. |
|
|
|
|
|
|
|
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 |
|
|
|
Added ``compression_options`` and ``on_message_callback``. |
|
|
|
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: |
|
|
|
io_loop = IOLoop.current() |
|
|
@ -1066,7 +1227,10 @@ def websocket_connect(url, io_loop=None, callback=None, connect_timeout=None, |
|
|
|
request, httpclient.HTTPRequest._DEFAULTS) |
|
|
|
conn = WebSocketClientConnection(io_loop, request, |
|
|
|
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: |
|
|
|
io_loop.add_future(conn.connect_future, callback) |
|
|
|
return conn.connect_future |
|
|
|