Created
August 16, 2010 10:21
-
-
Save sris/526746 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment