|
|
@@ -0,0 +1,263 @@ |
|
|
# Author: Jacob Kristhammar, 2010 |
|
|
# |
|
|
# Updated version of websocket.py[1] that implements latest[2] stable version |
|
|
# of the websocket protocol. |
|
|
# |
|
|
# NB. It's no longer possible to manually select which callback that should |
|
|
# be invoked upon message reception. Instead you must override the |
|
|
# on_message(message) method to handle incoming messsages. |
|
|
# This also means that you don't have to explicitly invoke |
|
|
# receive_message, in fact you shouldn't. |
|
|
# |
|
|
# [1] http://github.com/facebook/tornado/blob/ |
|
|
# 2c89b89536bbfa081745336bb5ab5465c448cb8a/tornado/websocket.py |
|
|
# [2] http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 |
|
|
|
|
|
import functools |
|
|
import hashlib |
|
|
import logging |
|
|
import re |
|
|
import struct |
|
|
import time |
|
|
import tornado.escape |
|
|
import tornado.web |
|
|
|
|
|
|
|
|
class WebSocketHandler(tornado.web.RequestHandler): |
|
|
"""Subclass this class to create a basic WebSocket handler. |
|
|
|
|
|
Override on_message to handle incoming messages. You can also override |
|
|
open and on_close to handle opened and closed connections. |
|
|
|
|
|
See http://www.w3.org/TR/2009/WD-websockets-20091222/ for details on the |
|
|
JavaScript interface. This implement the protocol as specified at |
|
|
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76. |
|
|
|
|
|
Here is an example Web Socket handler that echos back all received messages |
|
|
back to the client: |
|
|
|
|
|
class EchoWebSocket(websocket.WebSocketHandler): |
|
|
def open(self): |
|
|
print "WebSocket opened" |
|
|
|
|
|
def on_message(self, message): |
|
|
self.write_message(u"You said: " + message) |
|
|
|
|
|
def on_close(self): |
|
|
print "WebSocket closed" |
|
|
|
|
|
Web Sockets are not standard HTTP connections. The "handshake" is HTTP, |
|
|
but after the handshake, the protocol is message-based. Consequently, |
|
|
most of the Tornado HTTP facilities are not available in handlers of this |
|
|
type. The only communication methods available to you are send_message() |
|
|
and close(). Likewise, your request handler class should |
|
|
implement open() method rather than get() or post(). |
|
|
|
|
|
If you map the handler above to "/websocket" in your application, you can |
|
|
invoke it in JavaScript with: |
|
|
|
|
|
var ws = new WebSocket("ws://localhost:8888/websocket"); |
|
|
ws.onopen = function() { |
|
|
ws.send("Hello, world"); |
|
|
}; |
|
|
ws.onmessage = function (evt) { |
|
|
alert(evt.data); |
|
|
}; |
|
|
|
|
|
This script pops up an alert box that says "You said: Hello, world". |
|
|
""" |
|
|
def __init__(self, application, request): |
|
|
tornado.web.RequestHandler.__init__(self, application, request) |
|
|
self.stream = request.connection.stream |
|
|
self.client_terminated = False |
|
|
self._waiting = None |
|
|
|
|
|
def _execute(self, transforms, *args, **kwargs): |
|
|
self.open_args = args |
|
|
self.open_kwargs = kwargs |
|
|
try: |
|
|
self.ws_request = WebSocketRequest(self.request) |
|
|
except ValueError: |
|
|
logging.debug("Malformed WebSocket request received") |
|
|
self._abort() |
|
|
return |
|
|
self.stream.read_bytes(8, self._handle_challenge) |
|
|
|
|
|
def _handle_challenge(self, challenge): |
|
|
try: |
|
|
challenge_response = self.ws_request.challenge_response(challenge) |
|
|
except ValueError: |
|
|
logging.debug("Malformed key data in WebSocket request") |
|
|
self._abort() |
|
|
return |
|
|
self._write_response(challenge_response) |
|
|
|
|
|
def _write_response(self, challenge): |
|
|
self.stream.write( |
|
|
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" |
|
|
"Upgrade: WebSocket\r\n" |
|
|
"Connection: Upgrade\r\n" |
|
|
"Server: TornadoServer/0.1\r\n" |
|
|
"Sec-WebSocket-Origin: %s\r\n" |
|
|
"Sec-WebSocket-Location: ws://%s%s\r\n" |
|
|
"\r\n%s" % (self.request.headers["Origin"], self.request.host, |
|
|
self.request.path, challenge)) |
|
|
self.async_callback(self.open)(*self.open_args, **self.open_kwargs) |
|
|
self._receive_message() |
|
|
|
|
|
def write_message(self, message): |
|
|
"""Sends the given message to the client of this Web Socket.""" |
|
|
if isinstance(message, dict): |
|
|
message = tornado.escape.json_encode(message) |
|
|
if isinstance(message, unicode): |
|
|
message = message.encode("utf-8") |
|
|
assert isinstance(message, str) |
|
|
self.stream.write("\x00" + message + "\xff") |
|
|
|
|
|
def open(self, *args, **kwargs): |
|
|
"""Invoked when a new WebSocket is opened.""" |
|
|
pass |
|
|
|
|
|
def on_message(self, message): |
|
|
"""Handle incoming messages on the WebSocket |
|
|
|
|
|
This method must be overloaded |
|
|
""" |
|
|
raise NotImplementedError |
|
|
|
|
|
def on_close(self): |
|
|
"""Invoked when the WebSocket is closed.""" |
|
|
pass |
|
|
|
|
|
|
|
|
def close(self): |
|
|
"""Closes this Web Socket. |
|
|
|
|
|
Once the close handshake is successful the socket will be closed. |
|
|
""" |
|
|
if self.client_terminated and self._waiting: |
|
|
tornado.ioloop.IOLoop.instance().remove_timeout(self._waiting) |
|
|
self.stream.close() |
|
|
else: |
|
|
self.stream.write("\xff\x00") |
|
|
self._waiting = tornado.ioloop.IOLoop.instance().add_timeout( |
|
|
time.time() + 5, self._abort) |
|
|
|
|
|
def async_callback(self, callback, *args, **kwargs): |
|
|
"""Wrap callbacks with this if they are used on asynchronous requests. |
|
|
|
|
|
Catches exceptions properly and closes this Web Socket if an exception |
|
|
is uncaught. |
|
|
""" |
|
|
if args or kwargs: |
|
|
callback = functools.partial(callback, *args, **kwargs) |
|
|
def wrapper(*args, **kwargs): |
|
|
try: |
|
|
return callback(*args, **kwargs) |
|
|
except Exception, e: |
|
|
logging.error("Uncaught exception in %s", |
|
|
self.request.path, exc_info=True) |
|
|
self._abort() |
|
|
return wrapper |
|
|
|
|
|
def _abort(self): |
|
|
"""Instantly aborts the WebSocket connection by closing the socket""" |
|
|
self.client_terminated = True |
|
|
self.stream.close() |
|
|
|
|
|
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("\xff", self._on_end_delimiter) |
|
|
elif frame_type == 0xff: |
|
|
self.stream.read_bytes(1, self._on_length_indicator) |
|
|
else: |
|
|
self._abort() |
|
|
|
|
|
def _on_end_delimiter(self, frame): |
|
|
if not self.client_terminated: |
|
|
self.async_callback(self.on_message)( |
|
|
frame[:-1].decode("utf-8", "replace")) |
|
|
self._receive_message() |
|
|
|
|
|
def _on_length_indicator(self, byte): |
|
|
if ord(byte) != 0x00: |
|
|
self._abort() |
|
|
return |
|
|
self.client_terminated = True |
|
|
self.close() |
|
|
|
|
|
def on_connection_close(self): |
|
|
self.client_terminated = True |
|
|
self.on_close() |
|
|
|
|
|
def _not_supported(self, *args, **kwargs): |
|
|
raise Exception("Method not supported for Web Sockets") |
|
|
|
|
|
|
|
|
for method in ["write", "redirect", "set_header", "send_error", "set_cookie", |
|
|
"set_status", "flush", "finish"]: |
|
|
setattr(WebSocketHandler, method, WebSocketHandler._not_supported) |
|
|
|
|
|
|
|
|
class WebSocketRequest(object): |
|
|
"""A single WebSocket request. |
|
|
|
|
|
This class provides basic functionality to process WebSockets requests as |
|
|
specified in |
|
|
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 |
|
|
""" |
|
|
def __init__(self, request): |
|
|
self.request = request |
|
|
self.challenge = None |
|
|
self._handle_websocket_headers() |
|
|
|
|
|
def challenge_response(self, challenge): |
|
|
"""Generates the challange 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_websocket_headers(self): |
|
|
"""Verifies all invariant- and required headers |
|
|
|
|
|
If a header is missing or have an incorrect value ValueError will be |
|
|
raised |
|
|
""" |
|
|
headers = self.request.headers |
|
|
fields = ("Origin", "Host", "Sec-Websocket-Key1", |
|
|
"Sec-Websocket-Key2") |
|
|
if headers.get("Upgrade", '').lower() != "websocket" or \ |
|
|
headers.get("Connection", '').lower() != "upgrade" or \ |
|
|
not all(map(lambda f: self.request.headers.get(f), fields)): |
|
|
raise ValueError("Missing/Invalid WebSocket headers") |
|
|
|
|
|
def _calculate_part(self, key): |
|
|
"""Processes the key headers and calculates their key value. |
|
|
|
|
|
Raises ValueError when feed invalid key.""" |
|
|
number, spaces = filter(str.isdigit, key), filter(str.isspace, key) |
|
|
try: |
|
|
key_number = int(number) / len(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() |