1343 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1343 lines
		
	
	
		
			50 KiB
		
	
	
	
		
			Python
		
	
	
| """Implementation of the WebSocket protocol.
 | |
| 
 | |
| `WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
 | |
| communication between the browser and server.
 | |
| 
 | |
| WebSockets are supported in the current versions of all major browsers,
 | |
| although older versions that do not support WebSockets are still in use
 | |
| (refer to http://caniuse.com/websockets for details).
 | |
| 
 | |
| This module implements the final version of the WebSocket protocol as
 | |
| defined in `RFC 6455 <http://tools.ietf.org/html/rfc6455>`_.  Certain
 | |
| browser versions (notably Safari 5.x) implemented an earlier draft of
 | |
| the protocol (known as "draft 76") and are not compatible with this module.
 | |
| 
 | |
| .. versionchanged:: 4.0
 | |
|    Removed support for the draft 76 protocol version.
 | |
| """
 | |
| 
 | |
| from __future__ import absolute_import, division, print_function
 | |
| 
 | |
| import base64
 | |
| import hashlib
 | |
| import os
 | |
| import sys
 | |
| import struct
 | |
| import tornado.escape
 | |
| import tornado.web
 | |
| import zlib
 | |
| 
 | |
| from tornado.concurrent import Future, future_set_result_unless_cancelled
 | |
| from tornado.escape import utf8, native_str, to_unicode
 | |
| from tornado import gen, httpclient, httputil
 | |
| from tornado.ioloop import IOLoop, PeriodicCallback
 | |
| from tornado.iostream import StreamClosedError
 | |
| from tornado.log import gen_log
 | |
| from tornado import simple_httpclient
 | |
| from tornado.queues import Queue
 | |
| from tornado.tcpclient import TCPClient
 | |
| from tornado.util import _websocket_mask, PY3
 | |
| 
 | |
| if PY3:
 | |
|     from urllib.parse import urlparse  # py2
 | |
|     xrange = range
 | |
| else:
 | |
|     from urlparse import urlparse  # py3
 | |
| 
 | |
| _default_max_message_size = 10 * 1024 * 1024
 | |
| 
 | |
| 
 | |
| class WebSocketError(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class WebSocketClosedError(WebSocketError):
 | |
|     """Raised by operations on a closed connection.
 | |
| 
 | |
|     .. versionadded:: 3.2
 | |
|     """
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class _DecompressTooLargeError(Exception):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class WebSocketHandler(tornado.web.RequestHandler):
 | |
|     """Subclass this class to create a basic WebSocket handler.
 | |
| 
 | |
|     Override `on_message` to handle incoming messages, and use
 | |
|     `write_message` to send messages to the client. You can also
 | |
|     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.
 | |
| 
 | |
|     Here is an example WebSocket handler that echos back all received messages
 | |
|     back to the client:
 | |
| 
 | |
|     .. testcode::
 | |
| 
 | |
|       class EchoWebSocket(tornado.websocket.WebSocketHandler):
 | |
|           def open(self):
 | |
|               print("WebSocket opened")
 | |
| 
 | |
|           def on_message(self, message):
 | |
|               self.write_message(u"You said: " + message)
 | |
| 
 | |
|           def on_close(self):
 | |
|               print("WebSocket closed")
 | |
| 
 | |
|     .. testoutput::
 | |
|        :hide:
 | |
| 
 | |
|     WebSockets are not standard HTTP connections. The "handshake" is
 | |
|     HTTP, but after the handshake, the protocol is
 | |
|     message-based. Consequently, most of the Tornado HTTP facilities
 | |
|     are not available in handlers of this type. The only communication
 | |
|     methods available to you are `write_message()`, `ping()`, and
 | |
|     `close()`. Likewise, your request handler class should implement
 | |
|     `open()` method rather than ``get()`` or ``post()``.
 | |
| 
 | |
|     If you map the handler above to ``/websocket`` in your application, you can
 | |
|     invoke it in JavaScript with::
 | |
| 
 | |
|       var ws = new WebSocket("ws://localhost:8888/websocket");
 | |
|       ws.onopen = function() {
 | |
|          ws.send("Hello, world");
 | |
|       };
 | |
|       ws.onmessage = function (evt) {
 | |
|          alert(evt.data);
 | |
|       };
 | |
| 
 | |
|     This script pops up an alert box that says "You said: Hello, world".
 | |
| 
 | |
|     Web browsers allow any site to open a websocket connection to any other,
 | |
|     instead of using the same-origin policy that governs other network
 | |
|     access from javascript.  This can be surprising and is a potential
 | |
|     security hole, so since Tornado 4.0 `WebSocketHandler` requires
 | |
|     applications that wish to receive cross-origin websockets to opt in
 | |
|     by overriding the `~WebSocketHandler.check_origin` method (see that
 | |
|     method's docs for details).  Failure to do so is the most likely
 | |
|     cause of 403 errors when making a websocket connection.
 | |
| 
 | |
|     When using a secure websocket connection (``wss://``) with a self-signed
 | |
|     certificate, the connection from a browser may fail because it wants
 | |
|     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)
 | |
|         self.ws_connection = None
 | |
|         self.close_code = None
 | |
|         self.close_reason = None
 | |
|         self.stream = None
 | |
|         self._on_close_called = False
 | |
| 
 | |
|     def get(self, *args, **kwargs):
 | |
|         self.open_args = args
 | |
|         self.open_kwargs = kwargs
 | |
| 
 | |
|         # Upgrade header should be present and should be equal to WebSocket
 | |
|         if self.request.headers.get("Upgrade", "").lower() != 'websocket':
 | |
|             self.set_status(400)
 | |
|             log_msg = "Can \"Upgrade\" only to \"WebSocket\"."
 | |
|             self.finish(log_msg)
 | |
|             gen_log.debug(log_msg)
 | |
|             return
 | |
| 
 | |
|         # Connection header should be upgrade.
 | |
|         # Some proxy servers/load balancers
 | |
|         # might mess with it.
 | |
|         headers = self.request.headers
 | |
|         connection = map(lambda s: s.strip().lower(),
 | |
|                          headers.get("Connection", "").split(","))
 | |
|         if 'upgrade' not in connection:
 | |
|             self.set_status(400)
 | |
|             log_msg = "\"Connection\" must be \"Upgrade\"."
 | |
|             self.finish(log_msg)
 | |
|             gen_log.debug(log_msg)
 | |
|             return
 | |
| 
 | |
|         # Handle WebSocket Origin naming convention differences
 | |
|         # The difference between version 8 and 13 is that in 8 the
 | |
|         # client sends a "Sec-Websocket-Origin" header and in 13 it's
 | |
|         # simply "Origin".
 | |
|         if "Origin" in self.request.headers:
 | |
|             origin = self.request.headers.get("Origin")
 | |
|         else:
 | |
|             origin = self.request.headers.get("Sec-Websocket-Origin", None)
 | |
| 
 | |
|         # If there was an origin header, check to make sure it matches
 | |
|         # according to check_origin. When the origin is None, we assume it
 | |
|         # did not come from a browser and that it can be passed on.
 | |
|         if origin is not None and not self.check_origin(origin):
 | |
|             self.set_status(403)
 | |
|             log_msg = "Cross origin websockets not allowed"
 | |
|             self.finish(log_msg)
 | |
|             gen_log.debug(log_msg)
 | |
|             return
 | |
| 
 | |
|         self.ws_connection = self.get_websocket_protocol()
 | |
|         if self.ws_connection:
 | |
|             self.ws_connection.accept_connection()
 | |
|         else:
 | |
|             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', _default_max_message_size)
 | |
| 
 | |
|     def write_message(self, message, binary=False):
 | |
|         """Sends the given message to the client of this Web Socket.
 | |
| 
 | |
|         The message may be either a string or a dict (which will be
 | |
|         encoded as json).  If the ``binary`` argument is false, the
 | |
|         message will be sent as utf8; in binary mode any byte string
 | |
|         is allowed.
 | |
| 
 | |
|         If the connection is already closed, raises `WebSocketClosedError`.
 | |
|         Returns a `.Future` which can be used for flow control.
 | |
| 
 | |
|         .. versionchanged:: 3.2
 | |
|            `WebSocketClosedError` was added (previously a closed connection
 | |
|            would raise an `AttributeError`)
 | |
| 
 | |
|         .. versionchanged:: 4.3
 | |
|            Returns a `.Future` which can be used for flow control.
 | |
| 
 | |
|         .. versionchanged:: 5.0
 | |
|            Consistently raises `WebSocketClosedError`. Previously could
 | |
|            sometimes raise `.StreamClosedError`.
 | |
|         """
 | |
|         if self.ws_connection is None:
 | |
|             raise WebSocketClosedError()
 | |
|         if isinstance(message, dict):
 | |
|             message = tornado.escape.json_encode(message)
 | |
|         return self.ws_connection.write_message(message, binary=binary)
 | |
| 
 | |
|     def select_subprotocol(self, subprotocols):
 | |
|         """Override to implement subprotocol negotiation.
 | |
| 
 | |
|         ``subprotocols`` is a list of strings identifying the
 | |
|         subprotocols proposed by the client.  This method may be
 | |
|         overridden to return one of those strings to select it, or
 | |
|         ``None`` to not select a subprotocol.
 | |
| 
 | |
|         Failure to select a subprotocol does not automatically abort
 | |
|         the connection, although clients may close the connection if
 | |
|         none of their proposed subprotocols was selected.
 | |
| 
 | |
|         The list may be empty, in which case this method must return
 | |
|         None. This method is always called exactly once even if no
 | |
|         subprotocols were proposed so that the handler can be advised
 | |
|         of this fact.
 | |
| 
 | |
|         .. versionchanged:: 5.1
 | |
| 
 | |
|            Previously, this method was called with a list containing
 | |
|            an empty string instead of an empty list if no subprotocols
 | |
|            were proposed by the client.
 | |
|         """
 | |
|         return None
 | |
| 
 | |
|     @property
 | |
|     def selected_subprotocol(self):
 | |
|         """The subprotocol returned by `select_subprotocol`.
 | |
| 
 | |
|         .. versionadded:: 5.1
 | |
|         """
 | |
|         return self.ws_connection.selected_subprotocol
 | |
| 
 | |
|     def get_compression_options(self):
 | |
|         """Override to return compression options for the connection.
 | |
| 
 | |
|         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 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):
 | |
|         """Invoked when a new WebSocket is opened.
 | |
| 
 | |
|         The arguments to `open` are extracted from the `tornado.web.URLSpec`
 | |
|         regular expression, just like the arguments to
 | |
|         `tornado.web.RequestHandler.get`.
 | |
| 
 | |
|         `open` may be a coroutine. `on_message` will not be called until
 | |
|         `open` has returned.
 | |
| 
 | |
|         .. versionchanged:: 5.1
 | |
| 
 | |
|            ``open`` may be a coroutine.
 | |
|         """
 | |
|         pass
 | |
| 
 | |
|     def on_message(self, message):
 | |
|         """Handle incoming messages on the WebSocket
 | |
| 
 | |
|         This method must be overridden.
 | |
| 
 | |
|         .. versionchanged:: 4.5
 | |
| 
 | |
|            ``on_message`` can be a coroutine.
 | |
|         """
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def ping(self, data=b''):
 | |
|         """Send ping frame to the remote end.
 | |
| 
 | |
|         The data argument allows a small amount of data (up to 125
 | |
|         bytes) to be sent as a part of the ping message. Note that not
 | |
|         all websocket implementations expose this data to
 | |
|         applications.
 | |
| 
 | |
|         Consider using the ``websocket_ping_interval`` application
 | |
|         setting instead of sending pings manually.
 | |
| 
 | |
|         .. versionchanged:: 5.1
 | |
| 
 | |
|            The data argument is now optional.
 | |
| 
 | |
|         """
 | |
|         data = utf8(data)
 | |
|         if self.ws_connection is None:
 | |
|             raise WebSocketClosedError()
 | |
|         self.ws_connection.write_ping(data)
 | |
| 
 | |
|     def on_pong(self, data):
 | |
|         """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.
 | |
| 
 | |
|         If the connection was closed cleanly and a status code or reason
 | |
|         phrase was supplied, these values will be available as the attributes
 | |
|         ``self.close_code`` and ``self.close_reason``.
 | |
| 
 | |
|         .. versionchanged:: 4.0
 | |
| 
 | |
|            Added ``close_code`` and ``close_reason`` attributes.
 | |
|         """
 | |
|         pass
 | |
| 
 | |
|     def close(self, code=None, reason=None):
 | |
|         """Closes this Web Socket.
 | |
| 
 | |
|         Once the close handshake is successful the socket will be closed.
 | |
| 
 | |
|         ``code`` may be a numeric status code, taken from the values
 | |
|         defined in `RFC 6455 section 7.4.1
 | |
|         <https://tools.ietf.org/html/rfc6455#section-7.4.1>`_.
 | |
|         ``reason`` may be a textual message about why the connection is
 | |
|         closing.  These values are made available to the client, but are
 | |
|         not otherwise interpreted by the websocket protocol.
 | |
| 
 | |
|         .. versionchanged:: 4.0
 | |
| 
 | |
|            Added the ``code`` and ``reason`` arguments.
 | |
|         """
 | |
|         if self.ws_connection:
 | |
|             self.ws_connection.close(code, reason)
 | |
|             self.ws_connection = None
 | |
| 
 | |
|     def check_origin(self, origin):
 | |
|         """Override to enable support for allowing alternate origins.
 | |
| 
 | |
|         The ``origin`` argument is the value of the ``Origin`` HTTP
 | |
|         header, the url responsible for initiating this request.  This
 | |
|         method is not called for clients that do not send this header;
 | |
|         such requests are always allowed (because all browsers that
 | |
|         implement WebSockets support this header, and non-browser
 | |
|         clients do not have the same cross-site security concerns).
 | |
| 
 | |
|         Should return True to accept the request or False to reject it.
 | |
|         By default, rejects all requests with an origin on a host other
 | |
|         than this one.
 | |
| 
 | |
|         This is a security protection against cross site scripting attacks on
 | |
|         browsers, since WebSockets are allowed to bypass the usual same-origin
 | |
|         policies and don't use CORS headers.
 | |
| 
 | |
|         .. warning::
 | |
| 
 | |
|            This is an important security measure; don't disable it
 | |
|            without understanding the security implications. In
 | |
|            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
 | |
|            <https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html>`_
 | |
|            `articles
 | |
|            <https://devcenter.heroku.com/articles/websocket-security>`_
 | |
|            for more.
 | |
| 
 | |
|         To accept all cross-origin traffic (which was the default prior to
 | |
|         Tornado 4.0), simply override this method to always return true::
 | |
| 
 | |
|             def check_origin(self, origin):
 | |
|                 return True
 | |
| 
 | |
|         To allow connections from any subdomain of your site, you might
 | |
|         do something like::
 | |
| 
 | |
|             def check_origin(self, origin):
 | |
|                 parsed_origin = urllib.parse.urlparse(origin)
 | |
|                 return parsed_origin.netloc.endswith(".mydomain.com")
 | |
| 
 | |
|         .. versionadded:: 4.0
 | |
| 
 | |
|         """
 | |
|         parsed_origin = urlparse(origin)
 | |
|         origin = parsed_origin.netloc
 | |
|         origin = origin.lower()
 | |
| 
 | |
|         host = self.request.headers.get("Host")
 | |
| 
 | |
|         # Check to see that origin matches host directly, including ports
 | |
|         return origin == host
 | |
| 
 | |
|     def set_nodelay(self, value):
 | |
|         """Set the no-delay flag for this stream.
 | |
| 
 | |
|         By default, small messages may be delayed and/or combined to minimize
 | |
|         the number of packets sent.  This can sometimes cause 200-500ms delays
 | |
|         due to the interaction between Nagle's algorithm and TCP delayed
 | |
|         ACKs.  To reduce this delay (at the expense of possibly increasing
 | |
|         bandwidth usage), call ``self.set_nodelay(True)`` once the websocket
 | |
|         connection is established.
 | |
| 
 | |
|         See `.BaseIOStream.set_nodelay` for additional details.
 | |
| 
 | |
|         .. versionadded:: 3.1
 | |
|         """
 | |
|         self.stream.set_nodelay(value)
 | |
| 
 | |
|     def on_connection_close(self):
 | |
|         if self.ws_connection:
 | |
|             self.ws_connection.on_connection_close()
 | |
|             self.ws_connection = None
 | |
|         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:
 | |
|             super(WebSocketHandler, self).send_error(*args, **kwargs)
 | |
|         else:
 | |
|             # If we get an uncaught exception during the handshake,
 | |
|             # we have no choice but to abruptly close the connection.
 | |
|             # TODO: for uncaught exceptions after the handshake,
 | |
|             # we can close the connection more gracefully.
 | |
|             self.stream.close()
 | |
| 
 | |
|     def get_websocket_protocol(self):
 | |
|         websocket_version = self.request.headers.get("Sec-WebSocket-Version")
 | |
|         if websocket_version in ("7", "8", "13"):
 | |
|             return WebSocketProtocol13(
 | |
|                 self, compression_options=self.get_compression_options())
 | |
| 
 | |
|     def _attach_stream(self):
 | |
|         self.stream = self.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 _raise_not_supported_for_websockets(*args, **kwargs):
 | |
|     raise RuntimeError("Method not supported for Web Sockets")
 | |
| 
 | |
| 
 | |
| class WebSocketProtocol(object):
 | |
|     """Base class for WebSocket protocol versions.
 | |
|     """
 | |
|     def __init__(self, handler):
 | |
|         self.handler = handler
 | |
|         self.request = handler.request
 | |
|         self.stream = handler.stream
 | |
|         self.client_terminated = False
 | |
|         self.server_terminated = False
 | |
| 
 | |
|     def _run_callback(self, callback, *args, **kwargs):
 | |
|         """Runs the given callback with exception handling.
 | |
| 
 | |
|         If the callback is a coroutine, returns its Future. On error, aborts the
 | |
|         websocket connection and returns None.
 | |
|         """
 | |
|         try:
 | |
|             result = callback(*args, **kwargs)
 | |
|         except Exception:
 | |
|             self.handler.log_exception(*sys.exc_info())
 | |
|             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()
 | |
| 
 | |
|     def _abort(self):
 | |
|         """Instantly aborts the WebSocket connection by closing the socket"""
 | |
|         self.client_terminated = True
 | |
|         self.server_terminated = True
 | |
|         self.stream.close()  # forcibly tear down the connection
 | |
|         self.close()  # let the subclass cleanup
 | |
| 
 | |
| 
 | |
| class _PerMessageDeflateCompressor(object):
 | |
|     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.
 | |
|         if not (8 <= max_wbits <= zlib.MAX_WBITS):
 | |
|             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(self._compression_level,
 | |
|                                 zlib.DEFLATED, -self._max_wbits, self._mem_level)
 | |
| 
 | |
|     def compress(self, data):
 | |
|         compressor = self._compressor or self._create_compressor()
 | |
|         data = (compressor.compress(data) +
 | |
|                 compressor.flush(zlib.Z_SYNC_FLUSH))
 | |
|         assert data.endswith(b'\x00\x00\xff\xff')
 | |
|         return data[:-4]
 | |
| 
 | |
| 
 | |
| class _PerMessageDeflateDecompressor(object):
 | |
|     def __init__(self, persistent, max_wbits, max_message_size, compression_options=None):
 | |
|         self._max_message_size = max_message_size
 | |
|         if max_wbits is None:
 | |
|             max_wbits = zlib.MAX_WBITS
 | |
|         if not (8 <= max_wbits <= zlib.MAX_WBITS):
 | |
|             raise ValueError("Invalid max_wbits value %r; allowed range 8-%d",
 | |
|                              max_wbits, zlib.MAX_WBITS)
 | |
|         self._max_wbits = max_wbits
 | |
|         if persistent:
 | |
|             self._decompressor = self._create_decompressor()
 | |
|         else:
 | |
|             self._decompressor = None
 | |
| 
 | |
|     def _create_decompressor(self):
 | |
|         return zlib.decompressobj(-self._max_wbits)
 | |
| 
 | |
|     def decompress(self, data):
 | |
|         decompressor = self._decompressor or self._create_decompressor()
 | |
|         result = decompressor.decompress(data + b'\x00\x00\xff\xff', self._max_message_size)
 | |
|         if decompressor.unconsumed_tail:
 | |
|             raise _DecompressTooLargeError()
 | |
|         return result
 | |
| 
 | |
| 
 | |
| class WebSocketProtocol13(WebSocketProtocol):
 | |
|     """Implementation of the WebSocket protocol from RFC 6455.
 | |
| 
 | |
|     This class supports versions 7 and 8 of the protocol in addition to the
 | |
|     final version 13.
 | |
|     """
 | |
|     # Bit masks for the first byte of a frame.
 | |
|     FIN = 0x80
 | |
|     RSV1 = 0x40
 | |
|     RSV2 = 0x20
 | |
|     RSV3 = 0x10
 | |
|     RSV_MASK = RSV1 | RSV2 | RSV3
 | |
|     OPCODE_MASK = 0x0f
 | |
| 
 | |
|     def __init__(self, handler, mask_outgoing=False,
 | |
|                  compression_options=None):
 | |
|         WebSocketProtocol.__init__(self, handler)
 | |
|         self.mask_outgoing = mask_outgoing
 | |
|         self._final_frame = False
 | |
|         self._frame_opcode = None
 | |
|         self._masked_frame = None
 | |
|         self._frame_mask = None
 | |
|         self._frame_length = None
 | |
|         self._fragmented_message_buffer = None
 | |
|         self._fragmented_message_opcode = None
 | |
|         self._waiting = None
 | |
|         self._compression_options = compression_options
 | |
|         self._decompressor = None
 | |
|         self._compressor = None
 | |
|         self._frame_compressed = None
 | |
|         # The total uncompressed size of all messages received or sent.
 | |
|         # Unicode messages are encoded to utf8.
 | |
|         # Only for testing; subject to change.
 | |
|         self._message_bytes_in = 0
 | |
|         self._message_bytes_out = 0
 | |
|         # The total size of all packets received or sent.  Includes
 | |
|         # 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:
 | |
|             self._handle_websocket_headers()
 | |
|         except ValueError:
 | |
|             self.handler.set_status(400)
 | |
|             log_msg = "Missing/Invalid WebSocket headers"
 | |
|             self.handler.finish(log_msg)
 | |
|             gen_log.debug(log_msg)
 | |
|             return
 | |
| 
 | |
|         try:
 | |
|             self._accept_connection()
 | |
|         except ValueError:
 | |
|             gen_log.debug("Malformed WebSocket request received",
 | |
|                           exc_info=True)
 | |
|             self._abort()
 | |
|             return
 | |
| 
 | |
|     def _handle_websocket_headers(self):
 | |
|         """Verifies all invariant- and required headers
 | |
| 
 | |
|         If a header is missing or have an incorrect value ValueError will be
 | |
|         raised
 | |
|         """
 | |
|         fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
 | |
|         if not all(map(lambda f: self.request.headers.get(f), fields)):
 | |
|             raise ValueError("Missing/Invalid WebSocket headers")
 | |
| 
 | |
|     @staticmethod
 | |
|     def compute_accept_value(key):
 | |
|         """Computes the value for the Sec-WebSocket-Accept header,
 | |
|         given the value for Sec-WebSocket-Key.
 | |
|         """
 | |
|         sha1 = hashlib.sha1()
 | |
|         sha1.update(utf8(key))
 | |
|         sha1.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")  # Magic value
 | |
|         return native_str(base64.b64encode(sha1.digest()))
 | |
| 
 | |
|     def _challenge_response(self):
 | |
|         return WebSocketProtocol13.compute_accept_value(
 | |
|             self.request.headers.get("Sec-Websocket-Key"))
 | |
| 
 | |
|     @gen.coroutine
 | |
|     def _accept_connection(self):
 | |
|         subprotocol_header = self.request.headers.get("Sec-WebSocket-Protocol")
 | |
|         if subprotocol_header:
 | |
|             subprotocols = [s.strip() for s in subprotocol_header.split(',')]
 | |
|         else:
 | |
|             subprotocols = []
 | |
|         self.selected_subprotocol = self.handler.select_subprotocol(subprotocols)
 | |
|         if self.selected_subprotocol:
 | |
|             assert self.selected_subprotocol in subprotocols
 | |
|             self.handler.set_header("Sec-WebSocket-Protocol", self.selected_subprotocol)
 | |
| 
 | |
|         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._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']
 | |
|                 self.handler.set_header("Sec-WebSocket-Extensions",
 | |
|                                         httputil._encode_header(
 | |
|                                             'permessage-deflate', ext[1]))
 | |
|                 break
 | |
| 
 | |
|         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()
 | |
|         open_result = self._run_callback(self.handler.open, *self.handler.open_args,
 | |
|                                          **self.handler.open_kwargs)
 | |
|         if open_result is not None:
 | |
|             yield open_result
 | |
|         yield self._receive_frame_loop()
 | |
| 
 | |
|     def _parse_extensions_header(self, headers):
 | |
|         extensions = headers.get("Sec-WebSocket-Extensions", '')
 | |
|         if extensions:
 | |
|             return [httputil._parse_header(e.strip())
 | |
|                     for e in extensions.split(',')]
 | |
|         return []
 | |
| 
 | |
|     def _process_server_headers(self, key, headers):
 | |
|         """Process the headers sent by the server to this client connection.
 | |
| 
 | |
|         'key' is the websocket handshake challenge/response key.
 | |
|         """
 | |
|         assert headers['Upgrade'].lower() == 'websocket'
 | |
|         assert headers['Connection'].lower() == 'upgrade'
 | |
|         accept = self.compute_accept_value(key)
 | |
|         assert headers['Sec-Websocket-Accept'] == accept
 | |
| 
 | |
|         extensions = self._parse_extensions_header(headers)
 | |
|         for ext in extensions:
 | |
|             if (ext[0] == 'permessage-deflate' and
 | |
|                     self._compression_options is not None):
 | |
|                 self._create_compressors('client', ext[1])
 | |
|             else:
 | |
|                 raise ValueError("unsupported extension %r", ext)
 | |
| 
 | |
|         self.selected_subprotocol = headers.get('Sec-WebSocket-Protocol', None)
 | |
| 
 | |
|     def _get_compressor_options(self, side, agreed_parameters, compression_options=None):
 | |
|         """Converts a websocket agreed_parameters set to keyword arguments
 | |
|         for our compressor objects.
 | |
|         """
 | |
|         options = dict(
 | |
|             persistent=(side + '_no_context_takeover') not in agreed_parameters)
 | |
|         wbits_header = agreed_parameters.get(side + '_max_window_bits', None)
 | |
|         if wbits_header is None:
 | |
|             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, compression_options=None):
 | |
|         # TODO: handle invalid parameters gracefully
 | |
|         allowed_keys = set(['server_no_context_takeover',
 | |
|                             'client_no_context_takeover',
 | |
|                             'server_max_window_bits',
 | |
|                             'client_max_window_bits'])
 | |
|         for key in agreed_parameters:
 | |
|             if key not in allowed_keys:
 | |
|                 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, compression_options))
 | |
|         self._decompressor = _PerMessageDeflateDecompressor(
 | |
|             max_message_size=self.handler.max_message_size,
 | |
|             **self._get_compressor_options(other_side, agreed_parameters, compression_options))
 | |
| 
 | |
|     def _write_frame(self, fin, opcode, data, flags=0):
 | |
|         data_len = len(data)
 | |
|         if opcode & 0x8:
 | |
|             # All control frames MUST have a payload length of 125
 | |
|             # bytes or less and MUST NOT be fragmented.
 | |
|             if not fin:
 | |
|                 raise ValueError("control frames may not be fragmented")
 | |
|             if data_len > 125:
 | |
|                 raise ValueError("control frame payloads may not exceed 125 bytes")
 | |
|         if fin:
 | |
|             finbit = self.FIN
 | |
|         else:
 | |
|             finbit = 0
 | |
|         frame = struct.pack("B", finbit | opcode | flags)
 | |
|         if self.mask_outgoing:
 | |
|             mask_bit = 0x80
 | |
|         else:
 | |
|             mask_bit = 0
 | |
|         if data_len < 126:
 | |
|             frame += struct.pack("B", data_len | mask_bit)
 | |
|         elif data_len <= 0xFFFF:
 | |
|             frame += struct.pack("!BH", 126 | mask_bit, data_len)
 | |
|         else:
 | |
|             frame += struct.pack("!BQ", 127 | mask_bit, data_len)
 | |
|         if self.mask_outgoing:
 | |
|             mask = os.urandom(4)
 | |
|             data = mask + _websocket_mask(mask, data)
 | |
|         frame += data
 | |
|         self._wire_bytes_out += len(frame)
 | |
|         return self.stream.write(frame)
 | |
| 
 | |
|     def write_message(self, message, binary=False):
 | |
|         """Sends the given message to the client of this Web Socket."""
 | |
|         if binary:
 | |
|             opcode = 0x2
 | |
|         else:
 | |
|             opcode = 0x1
 | |
|         message = tornado.escape.utf8(message)
 | |
|         assert isinstance(message, bytes)
 | |
|         self._message_bytes_out += len(message)
 | |
|         flags = 0
 | |
|         if self._compressor:
 | |
|             message = self._compressor.compress(message)
 | |
|             flags |= self.RSV1
 | |
|         # For historical reasons, write methods in Tornado operate in a semi-synchronous
 | |
|         # mode in which awaiting the Future they return is optional (But errors can
 | |
|         # still be raised). This requires us to go through an awkward dance here
 | |
|         # to transform the errors that may be returned while presenting the same
 | |
|         # semi-synchronous interface.
 | |
|         try:
 | |
|             fut = self._write_frame(True, opcode, message, flags=flags)
 | |
|         except StreamClosedError:
 | |
|             raise WebSocketClosedError()
 | |
| 
 | |
|         @gen.coroutine
 | |
|         def wrapper():
 | |
|             try:
 | |
|                 yield fut
 | |
|             except StreamClosedError:
 | |
|                 raise WebSocketClosedError()
 | |
|         return wrapper()
 | |
| 
 | |
|     def write_ping(self, data):
 | |
|         """Send ping frame."""
 | |
|         assert isinstance(data, bytes)
 | |
|         self._write_frame(True, 0x9, data)
 | |
| 
 | |
|     @gen.coroutine
 | |
|     def _receive_frame_loop(self):
 | |
|         try:
 | |
|             while not self.client_terminated:
 | |
|                 yield self._receive_frame()
 | |
|         except StreamClosedError:
 | |
|             self._abort()
 | |
| 
 | |
|     def _read_bytes(self, n):
 | |
|         self._wire_bytes_in += n
 | |
|         return self.stream.read_bytes(n)
 | |
| 
 | |
|     @gen.coroutine
 | |
|     def _receive_frame(self):
 | |
|         # Read the frame header.
 | |
|         data = yield self._read_bytes(2)
 | |
|         header, mask_payloadlen = struct.unpack("BB", data)
 | |
|         is_final_frame = header & self.FIN
 | |
|         reserved_bits = header & self.RSV_MASK
 | |
|         opcode = header & self.OPCODE_MASK
 | |
|         opcode_is_control = opcode & 0x8
 | |
|         if self._decompressor is not None and opcode != 0:
 | |
|             # Compression flag is present in the first frame's header,
 | |
|             # but we can't decompress until we have all the frames of
 | |
|             # the message.
 | |
|             self._frame_compressed = bool(reserved_bits & self.RSV1)
 | |
|             reserved_bits &= ~self.RSV1
 | |
|         if reserved_bits:
 | |
|             # client is using as-yet-undefined extensions; abort
 | |
|             self._abort()
 | |
|             return
 | |
|         is_masked = bool(mask_payloadlen & 0x80)
 | |
|         payloadlen = mask_payloadlen & 0x7f
 | |
| 
 | |
|         # Parse and validate the length.
 | |
|         if opcode_is_control and payloadlen >= 126:
 | |
|             # control frames must have payload < 126
 | |
|             self._abort()
 | |
|             return
 | |
|         if payloadlen < 126:
 | |
|             self._frame_length = payloadlen
 | |
|         elif payloadlen == 126:
 | |
|             data = yield self._read_bytes(2)
 | |
|             payloadlen = struct.unpack("!H", data)[0]
 | |
|         elif payloadlen == 127:
 | |
|             data = yield self._read_bytes(8)
 | |
|             payloadlen = struct.unpack("!Q", data)[0]
 | |
|         new_len = payloadlen
 | |
|         if self._fragmented_message_buffer is not None:
 | |
|             new_len += len(self._fragmented_message_buffer)
 | |
|         if new_len > self.handler.max_message_size:
 | |
|             self.close(1009, "message too big")
 | |
|             self._abort()
 | |
|             return
 | |
| 
 | |
|         # Read the payload, unmasking if necessary.
 | |
|         if is_masked:
 | |
|             self._frame_mask = yield self._read_bytes(4)
 | |
|         data = yield self._read_bytes(payloadlen)
 | |
|         if is_masked:
 | |
|             data = _websocket_mask(self._frame_mask, data)
 | |
| 
 | |
|         # Decide what to do with this frame.
 | |
|         if opcode_is_control:
 | |
|             # control frames may be interleaved with a series of fragmented
 | |
|             # data frames, so control frames must not interact with
 | |
|             # self._fragmented_*
 | |
|             if not is_final_frame:
 | |
|                 # control frames must not be fragmented
 | |
|                 self._abort()
 | |
|                 return
 | |
|         elif opcode == 0:  # continuation frame
 | |
|             if self._fragmented_message_buffer is None:
 | |
|                 # nothing to continue
 | |
|                 self._abort()
 | |
|                 return
 | |
|             self._fragmented_message_buffer += data
 | |
|             if is_final_frame:
 | |
|                 opcode = self._fragmented_message_opcode
 | |
|                 data = self._fragmented_message_buffer
 | |
|                 self._fragmented_message_buffer = None
 | |
|         else:  # start of new data message
 | |
|             if self._fragmented_message_buffer is not None:
 | |
|                 # can't start new message until the old one is finished
 | |
|                 self._abort()
 | |
|                 return
 | |
|             if not is_final_frame:
 | |
|                 self._fragmented_message_opcode = opcode
 | |
|                 self._fragmented_message_buffer = data
 | |
| 
 | |
|         if is_final_frame:
 | |
|             handled_future = self._handle_message(opcode, data)
 | |
|             if handled_future is not None:
 | |
|                 yield handled_future
 | |
| 
 | |
|     def _handle_message(self, opcode, data):
 | |
|         """Execute on_message, returning its Future if it is a coroutine."""
 | |
|         if self.client_terminated:
 | |
|             return
 | |
| 
 | |
|         if self._frame_compressed:
 | |
|             try:
 | |
|                 data = self._decompressor.decompress(data)
 | |
|             except _DecompressTooLargeError:
 | |
|                 self.close(1009, "message too big after decompression")
 | |
|                 self._abort()
 | |
|                 return
 | |
| 
 | |
|         if opcode == 0x1:
 | |
|             # UTF-8 data
 | |
|             self._message_bytes_in += len(data)
 | |
|             try:
 | |
|                 decoded = data.decode("utf-8")
 | |
|             except UnicodeDecodeError:
 | |
|                 self._abort()
 | |
|                 return
 | |
|             return self._run_callback(self.handler.on_message, decoded)
 | |
|         elif opcode == 0x2:
 | |
|             # Binary data
 | |
|             self._message_bytes_in += len(data)
 | |
|             return self._run_callback(self.handler.on_message, data)
 | |
|         elif opcode == 0x8:
 | |
|             # Close
 | |
|             self.client_terminated = True
 | |
|             if len(data) >= 2:
 | |
|                 self.handler.close_code = struct.unpack('>H', data[:2])[0]
 | |
|             if len(data) > 2:
 | |
|                 self.handler.close_reason = to_unicode(data[2:])
 | |
|             # Echo the received close code, if any (RFC 6455 section 5.5.1).
 | |
|             self.close(self.handler.close_code)
 | |
|         elif opcode == 0x9:
 | |
|             # Ping
 | |
|             try:
 | |
|                 self._write_frame(True, 0xA, data)
 | |
|             except StreamClosedError:
 | |
|                 self._abort()
 | |
|             self._run_callback(self.handler.on_ping, data)
 | |
|         elif opcode == 0xA:
 | |
|             # Pong
 | |
|             self.last_pong = IOLoop.current().time()
 | |
|             return self._run_callback(self.handler.on_pong, data)
 | |
|         else:
 | |
|             self._abort()
 | |
| 
 | |
|     def close(self, code=None, reason=None):
 | |
|         """Closes the WebSocket connection."""
 | |
|         if not self.server_terminated:
 | |
|             if not self.stream.closed():
 | |
|                 if code is None and reason is not None:
 | |
|                     code = 1000  # "normal closure" status code
 | |
|                 if code is None:
 | |
|                     close_data = b''
 | |
|                 else:
 | |
|                     close_data = struct.pack('>H', code)
 | |
|                 if reason is not None:
 | |
|                     close_data += utf8(reason)
 | |
|                 try:
 | |
|                     self._write_frame(True, 0x8, close_data)
 | |
|                 except StreamClosedError:
 | |
|                     self._abort()
 | |
|             self.server_terminated = True
 | |
|         if self.client_terminated:
 | |
|             if self._waiting is not None:
 | |
|                 self.stream.io_loop.remove_timeout(self._waiting)
 | |
|                 self._waiting = None
 | |
|             self.stream.close()
 | |
|         elif self._waiting is None:
 | |
|             # Give the client a few seconds to complete a clean shutdown,
 | |
|             # otherwise just close the connection.
 | |
|             self._waiting = self.stream.io_loop.add_timeout(
 | |
|                 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.
 | |
| 
 | |
|     This class should not be instantiated directly; use the
 | |
|     `websocket_connect` function instead.
 | |
|     """
 | |
|     def __init__(self, request, on_message_callback=None,
 | |
|                  compression_options=None, ping_interval=None, ping_timeout=None,
 | |
|                  max_message_size=None, subprotocols=[]):
 | |
|         self.compression_options = compression_options
 | |
|         self.connect_future = Future()
 | |
|         self.protocol = None
 | |
|         self.read_queue = Queue(1)
 | |
|         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]
 | |
|         request.url = scheme + sep + rest
 | |
|         request.headers.update({
 | |
|             'Upgrade': 'websocket',
 | |
|             'Connection': 'Upgrade',
 | |
|             'Sec-WebSocket-Key': self.key,
 | |
|             'Sec-WebSocket-Version': '13',
 | |
|         })
 | |
|         if subprotocols is not None:
 | |
|             request.headers['Sec-WebSocket-Protocol'] = ','.join(subprotocols)
 | |
|         if self.compression_options is not None:
 | |
|             # Always offer to let the server set our max_wbits (and even though
 | |
|             # we don't offer it, we will accept a client_no_context_takeover
 | |
|             # from the server).
 | |
|             # TODO: set server parameters for deflate extension
 | |
|             # if requested in self.compression_options.
 | |
|             request.headers['Sec-WebSocket-Extensions'] = (
 | |
|                 'permessage-deflate; client_max_window_bits')
 | |
| 
 | |
|         self.tcp_client = TCPClient()
 | |
|         super(WebSocketClientConnection, self).__init__(
 | |
|             None, request, lambda: None, self._on_http_response,
 | |
|             104857600, self.tcp_client, 65536, 104857600)
 | |
| 
 | |
|     def close(self, code=None, reason=None):
 | |
|         """Closes the websocket connection.
 | |
| 
 | |
|         ``code`` and ``reason`` are documented under
 | |
|         `WebSocketHandler.close`.
 | |
| 
 | |
|         .. versionadded:: 3.2
 | |
| 
 | |
|         .. versionchanged:: 4.0
 | |
| 
 | |
|            Added the ``code`` and ``reason`` arguments.
 | |
|         """
 | |
|         if self.protocol is not None:
 | |
|             self.protocol.close(code, reason)
 | |
|             self.protocol = None
 | |
| 
 | |
|     def on_connection_close(self):
 | |
|         if not self.connect_future.done():
 | |
|             self.connect_future.set_exception(StreamClosedError())
 | |
|         self.on_message(None)
 | |
|         self.tcp_client.close()
 | |
|         super(WebSocketClientConnection, self).on_connection_close()
 | |
| 
 | |
|     def _on_http_response(self, response):
 | |
|         if not self.connect_future.done():
 | |
|             if response.error:
 | |
|                 self.connect_future.set_exception(response.error)
 | |
|             else:
 | |
|                 self.connect_future.set_exception(WebSocketError(
 | |
|                     "Non-websocket response"))
 | |
| 
 | |
|     def headers_received(self, start_line, headers):
 | |
|         if start_line.code != 101:
 | |
|             return super(WebSocketClientConnection, self).headers_received(
 | |
|                 start_line, headers)
 | |
| 
 | |
|         self.headers = headers
 | |
|         self.protocol = self.get_websocket_protocol()
 | |
|         self.protocol._process_server_headers(self.key, self.headers)
 | |
|         self.protocol.start_pinging()
 | |
|         IOLoop.current().add_callback(self.protocol._receive_frame_loop)
 | |
| 
 | |
|         if self._timeout is not None:
 | |
|             self.io_loop.remove_timeout(self._timeout)
 | |
|             self._timeout = None
 | |
| 
 | |
|         self.stream = self.connection.detach()
 | |
|         self.stream.set_close_callback(self.on_connection_close)
 | |
|         # Once we've taken over the connection, clear the final callback
 | |
|         # we set on the http request.  This deactivates the error handling
 | |
|         # in simple_httpclient that would otherwise interfere with our
 | |
|         # ability to see exceptions.
 | |
|         self.final_callback = None
 | |
| 
 | |
|         future_set_result_unless_cancelled(self.connect_future, self)
 | |
| 
 | |
|     def write_message(self, message, binary=False):
 | |
|         """Sends a message to the WebSocket server.
 | |
| 
 | |
|         If the stream is closed, raises `WebSocketClosedError`.
 | |
|         Returns a `.Future` which can be used for flow control.
 | |
| 
 | |
|         .. versionchanged:: 5.0
 | |
|            Exception raised on a closed stream changed from `.StreamClosedError`
 | |
|            to `WebSocketClosedError`.
 | |
|         """
 | |
|         return self.protocol.write_message(message, binary=binary)
 | |
| 
 | |
|     def read_message(self, callback=None):
 | |
|         """Reads a message from the WebSocket server.
 | |
| 
 | |
|         If on_message_callback was specified at WebSocket
 | |
|         initialization, this function will never return messages
 | |
| 
 | |
|         Returns a future whose result is the message, or None
 | |
|         if the connection is closed.  If a callback argument
 | |
|         is given it will be called with the future when it is
 | |
|         ready.
 | |
|         """
 | |
| 
 | |
|         future = self.read_queue.get()
 | |
|         if callback is not None:
 | |
|             self.io_loop.add_future(future, callback)
 | |
|         return future
 | |
| 
 | |
|     def on_message(self, message):
 | |
|         if self._on_message_callback:
 | |
|             self._on_message_callback(message)
 | |
|         else:
 | |
|             return self.read_queue.put(message)
 | |
| 
 | |
|     def ping(self, data=b''):
 | |
|         """Send ping frame to the remote end.
 | |
| 
 | |
|         The data argument allows a small amount of data (up to 125
 | |
|         bytes) to be sent as a part of the ping message. Note that not
 | |
|         all websocket implementations expose this data to
 | |
|         applications.
 | |
| 
 | |
|         Consider using the ``ping_interval`` argument to
 | |
|         `websocket_connect` instead of sending pings manually.
 | |
| 
 | |
|         .. versionadded:: 5.1
 | |
| 
 | |
|         """
 | |
|         data = utf8(data)
 | |
|         if self.protocol is None:
 | |
|             raise WebSocketClosedError()
 | |
|         self.protocol.write_ping(data)
 | |
| 
 | |
|     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)
 | |
| 
 | |
|     @property
 | |
|     def selected_subprotocol(self):
 | |
|         """The subprotocol selected by the server.
 | |
| 
 | |
|         .. versionadded:: 5.1
 | |
|         """
 | |
|         return self.protocol.selected_subprotocol
 | |
| 
 | |
| 
 | |
| def websocket_connect(url, callback=None, connect_timeout=None,
 | |
|                       on_message_callback=None, compression_options=None,
 | |
|                       ping_interval=None, ping_timeout=None,
 | |
|                       max_message_size=_default_max_message_size, subprotocols=None):
 | |
|     """Client-side websocket support.
 | |
| 
 | |
|     Takes a url and returns a Future whose result is a
 | |
|     `WebSocketClientConnection`.
 | |
| 
 | |
|     ``compression_options`` is interpreted in the same way as the
 | |
|     return value of `.WebSocketHandler.get_compression_options`.
 | |
| 
 | |
|     The connection supports two styles of operation. In the coroutine
 | |
|     style, the application typically calls
 | |
|     `~.WebSocketClientConnection.read_message` in a loop::
 | |
| 
 | |
|         conn = yield websocket_connect(url)
 | |
|         while True:
 | |
|             msg = yield conn.read_message()
 | |
|             if msg is None: break
 | |
|             # Do something with msg
 | |
| 
 | |
|     In the callback style, pass an ``on_message_callback`` to
 | |
|     ``websocket_connect``. In both styles, a message of ``None``
 | |
|     indicates that the connection has been closed.
 | |
| 
 | |
|     ``subprotocols`` may be a list of strings specifying proposed
 | |
|     subprotocols. The selected protocol may be found on the
 | |
|     ``selected_subprotocol`` attribute of the connection object
 | |
|     when the connection is complete.
 | |
| 
 | |
|     .. versionchanged:: 3.2
 | |
|        Also accepts ``HTTPRequest`` objects in place of urls.
 | |
| 
 | |
|     .. versionchanged:: 4.1
 | |
|        Added ``compression_options`` and ``on_message_callback``.
 | |
| 
 | |
|     .. versionchanged:: 4.5
 | |
|        Added the ``ping_interval``, ``ping_timeout``, and ``max_message_size``
 | |
|        arguments, which have the same meaning as in `WebSocketHandler`.
 | |
| 
 | |
|     .. versionchanged:: 5.0
 | |
|        The ``io_loop`` argument (deprecated since version 4.1) has been removed.
 | |
| 
 | |
|     .. versionchanged:: 5.1
 | |
|        Added the ``subprotocols`` argument.
 | |
|     """
 | |
|     if isinstance(url, httpclient.HTTPRequest):
 | |
|         assert connect_timeout is None
 | |
|         request = url
 | |
|         # Copy and convert the headers dict/object (see comments in
 | |
|         # AsyncHTTPClient.fetch)
 | |
|         request.headers = httputil.HTTPHeaders(request.headers)
 | |
|     else:
 | |
|         request = httpclient.HTTPRequest(url, connect_timeout=connect_timeout)
 | |
|     request = httpclient._RequestProxy(
 | |
|         request, httpclient.HTTPRequest._DEFAULTS)
 | |
|     conn = WebSocketClientConnection(request,
 | |
|                                      on_message_callback=on_message_callback,
 | |
|                                      compression_options=compression_options,
 | |
|                                      ping_interval=ping_interval,
 | |
|                                      ping_timeout=ping_timeout,
 | |
|                                      max_message_size=max_message_size,
 | |
|                                      subprotocols=subprotocols)
 | |
|     if callback is not None:
 | |
|         IOLoop.current().add_future(conn.connect_future, callback)
 | |
|     return conn.connect_future
 |