`WebSockets <http://dev.w3.org/html5/websockets/>`_ allow for bidirectional
communication between the browser and server.
-.. warning::
-
- The WebSocket protocol was recently finalized as `RFC 6455
- <http://tools.ietf.org/html/rfc6455>`_ and is not yet supported in
- all browsers. Refer to http://caniuse.com/websockets for details
- on compatibility. In addition, during development the protocol
- went through several incompatible versions, and some browsers only
- support older versions. By default this module only supports the
- latest version of the protocol, but optional support for an older
- version (known as "draft 76" or "hixie-76") can be enabled by
- overriding `WebSocketHandler.allow_draft76` (see that method's
- documentation for caveats).
+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, with_statement
import base64
import collections
-import functools
import hashlib
import os
import struct
-import time
import tornado.escape
import tornado.web
from tornado.log import gen_log, app_log
from tornado import simple_httpclient
from tornado.tcpclient import TCPClient
-from tornado.util import bytes_type, unicode_type, _websocket_mask
+from tornado.util import bytes_type, _websocket_mask
try:
from urllib.parse import urlparse # py2
if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
self.ws_connection = WebSocketProtocol13(self)
self.ws_connection.accept_connection()
- elif (self.allow_draft76() and
- "Sec-WebSocket-Version" not in self.request.headers):
- self.ws_connection = WebSocketProtocol76(self)
- self.ws_connection.accept_connection()
else:
self.stream.write(tornado.escape.utf8(
"HTTP/1.1 426 Upgrade Required\r\n"
closing. These values are made available to the client, but are
not otherwise interpreted by the websocket protocol.
- The ``code`` and ``reason`` arguments are ignored in the "draft76"
- protocol version.
-
.. versionchanged:: 4.0
Added the ``code`` and ``reason`` arguments.
# Check to see that origin matches host directly, including ports
return origin == host
- def allow_draft76(self):
- """Override to enable support for the older "draft76" protocol.
-
- The draft76 version of the websocket protocol is disabled by
- default due to security concerns, but it can be enabled by
- overriding this method to return True.
-
- Connections using the draft76 protocol do not support the
- ``binary=True`` flag to `write_message`.
-
- Support for the draft76 protocol is deprecated and will be
- removed in a future version of Tornado.
- """
- return False
-
def set_nodelay(self, value):
"""Set the no-delay flag for this stream.
"""
self.stream.set_nodelay(value)
- def get_websocket_scheme(self):
- """Return the url scheme used for this request, either "ws" or "wss".
-
- This is normally decided by HTTPServer, but applications
- may wish to override this if they are using an SSL proxy
- that does not provide the X-Scheme header as understood
- by HTTPServer.
-
- Note that this is only used by the draft76 protocol.
- """
- return "wss" if self.request.protocol == "https" else "ws"
-
def on_connection_close(self):
if self.ws_connection:
self.ws_connection.on_connection_close()
self.close() # let the subclass cleanup
-class WebSocketProtocol76(WebSocketProtocol):
- """Implementation of the WebSockets protocol, version hixie-76.
-
- This class provides basic functionality to process WebSockets requests as
- specified in
- http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
- """
- def __init__(self, handler):
- WebSocketProtocol.__init__(self, handler)
- self.challenge = None
- self._waiting = None
-
- def accept_connection(self):
- try:
- self._handle_websocket_headers()
- except ValueError:
- gen_log.debug("Malformed WebSocket request received")
- self._abort()
- return
-
- scheme = self.handler.get_websocket_scheme()
-
- # draft76 only allows a single subprotocol
- subprotocol_header = ''
- subprotocol = self.request.headers.get("Sec-WebSocket-Protocol", None)
- if subprotocol:
- selected = self.handler.select_subprotocol([subprotocol])
- if selected:
- assert selected == subprotocol
- subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
-
- # Write the initial headers before attempting to read the challenge.
- # This is necessary when using proxies (such as HAProxy), which
- # need to see the Upgrade headers before passing through the
- # non-HTTP traffic that follows.
- self.stream.write(tornado.escape.utf8(
- "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
- "Upgrade: WebSocket\r\n"
- "Connection: Upgrade\r\n"
- "Server: TornadoServer/%(version)s\r\n"
- "Sec-WebSocket-Origin: %(origin)s\r\n"
- "Sec-WebSocket-Location: %(scheme)s://%(host)s%(uri)s\r\n"
- "%(subprotocol)s"
- "\r\n" % (dict(
- version=tornado.version,
- origin=self.request.headers["Origin"],
- scheme=scheme,
- host=self.request.host,
- uri=self.request.uri,
- subprotocol=subprotocol_header))))
- self.stream.read_bytes(8, self._handle_challenge)
-
- def challenge_response(self, challenge):
- """Generates the challenge response that's needed in the handshake
-
- The challenge parameter should be the raw bytes as sent from the
- client.
- """
- key_1 = self.request.headers.get("Sec-Websocket-Key1")
- key_2 = self.request.headers.get("Sec-Websocket-Key2")
- try:
- part_1 = self._calculate_part(key_1)
- part_2 = self._calculate_part(key_2)
- except ValueError:
- raise ValueError("Invalid Keys/Challenge")
- return self._generate_challenge_response(part_1, part_2, challenge)
-
- def _handle_challenge(self, challenge):
- try:
- challenge_response = self.challenge_response(challenge)
- except ValueError:
- gen_log.debug("Malformed key data in WebSocket request")
- self._abort()
- return
- self._write_response(challenge_response)
-
- def _write_response(self, challenge):
- self.stream.write(challenge)
- self._run_callback(self.handler.open, *self.handler.open_args,
- **self.handler.open_kwargs)
- self._receive_message()
-
- def _handle_websocket_headers(self):
- """Verifies all invariant- and required headers
-
- If a header is missing or have an incorrect value ValueError will be
- raised
- """
- fields = ("Origin", "Host", "Sec-Websocket-Key1",
- "Sec-Websocket-Key2")
- if not all(map(lambda f: self.request.headers.get(f), fields)):
- raise ValueError("Missing/Invalid WebSocket headers")
-
- def _calculate_part(self, key):
- """Processes the key headers and calculates their key value.
-
- Raises ValueError when feed invalid key."""
- # pyflakes complains about variable reuse if both of these lines use 'c'
- number = int(''.join(c for c in key if c.isdigit()))
- spaces = len([c2 for c2 in key if c2.isspace()])
- try:
- key_number = number // spaces
- except (ValueError, ZeroDivisionError):
- raise ValueError
- return struct.pack(">I", key_number)
-
- def _generate_challenge_response(self, part_1, part_2, part_3):
- m = hashlib.md5()
- m.update(part_1)
- m.update(part_2)
- m.update(part_3)
- return m.digest()
-
- def _receive_message(self):
- self.stream.read_bytes(1, self._on_frame_type)
-
- def _on_frame_type(self, byte):
- frame_type = ord(byte)
- if frame_type == 0x00:
- self.stream.read_until(b"\xff", self._on_end_delimiter)
- elif frame_type == 0xff:
- self.stream.read_bytes(1, self._on_length_indicator)
- else:
- self._abort()
-
- def _on_end_delimiter(self, frame):
- if not self.client_terminated:
- self._run_callback(self.handler.on_message,
- frame[:-1].decode("utf-8", "replace"))
- if not self.client_terminated:
- self._receive_message()
-
- def _on_length_indicator(self, byte):
- if ord(byte) != 0x00:
- self._abort()
- return
- self.client_terminated = True
- self.close()
-
- def write_message(self, message, binary=False):
- """Sends the given message to the client of this Web Socket."""
- if binary:
- raise ValueError(
- "Binary messages not supported by this version of websockets")
- if isinstance(message, unicode_type):
- message = message.encode("utf-8")
- assert isinstance(message, bytes_type)
- self.stream.write(b"\x00" + message + b"\xff")
-
- def write_ping(self, data):
- """Send ping frame."""
- raise ValueError("Ping messages not supported by this version of websockets")
-
- def close(self, code=None, reason=None):
- """Closes the WebSocket connection."""
- if not self.server_terminated:
- if not self.stream.closed():
- self.stream.write("\xff\x00")
- self.server_terminated = True
- if self.client_terminated:
- if self._waiting is not None:
- self.stream.io_loop.remove_timeout(self._waiting)
- self._waiting = None
- self.stream.close()
- elif self._waiting is None:
- self._waiting = self.stream.io_loop.add_timeout(
- time.time() + 5, self._abort)
-
-
class WebSocketProtocol13(WebSocketProtocol):
"""Implementation of the WebSocket protocol from RFC 6455.