Commits

Ginés Martínez Sánchez committed edb2877 Draft

developing websocket

  • Participants
  • Parent commits 1114523

Comments (0)

Files changed (1)

ginsfsm/protocols/sockjs/server/websocket.py

-# -*- coding: utf-8 -*-
-"""
-    websocket
-
-    This module is a modified version of the WebSocketHandler from
-    tornado.sockjs adapted to ginsfsm.
-    "hixie-76/draft76" protocol is not supported.
-
-    This module contains modified version of the WebSocketHandler from
-    Tornado with some performance fixes and behavior changes for Hybi10
-    protocol.
-
-    Server-side implementation of the WebSocket protocol.
-
-    `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).
-"""
-# Author: Jacob Kristhammar, 2010
-
-import array
-import functools
-import hashlib
-import logging
-import struct
-import time
-import base64
-
-import ginsfsm.escape
-from ginsfsm.protocols.sockjs.server.session import ConnectionInfo
-from pyramid.httpexceptions import HTTPBadRequest
-from ginsfsm.protocols.wsgi.webob.websocket_response import WebsocketResponse
-
-
-# Fake byte literal support:  In python 2.6+, you can say b"foo" to get
-# a byte literal (str in 2.x, bytes in 3.x).  There's no way to do this
-# in a way that supports 2.5, though, so we need a function wrapper
-# to convert our string literals.  b() should only be applied to literal
-# latin1 strings.  Once we drop support for 2.5, we can remove this function
-# and just use byte literals.
-if str is unicode:
-    def b(s):
-        return s.encode('latin1')
-    bytes_type = bytes
-else:
-    def b(s):
-        return s
-    bytes_type = str
-
-# Support for 2.5
-try:
-    make_array = bytearray
-    to_string = bytes_type
-except NameError:
-    make_array = lambda d: array.array("B", d)
-    to_string = lambda d: d.tostring()
-
-
-class WebSocketHandler(object):
-    """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://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 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 write_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, context, request):
-        self.context = context
-        self.request = request
-        channel = request.environ['ginsfsm.channel']
-        self.stream = channel.gsock
-        self.ws_connection = None
-
-    def _execute(self):
-        self.open_args = None  # args
-        self.open_kwargs = None  # kwargs
-
-        # Websocket only supports GET method
-        response = self.request.response
-        if self.request.method != "GET":
-            response.status = 405
-            response.headers = (
-                ('Allow', 'GET'),
-            )
-            #self.stream.close()
-            return response
-
-        environ = self.request.environ
-
-        if 'websocket' not in environ.get('HTTP_UPGRADE', '').lower():
-            #self.stream.close()
-            return HTTPBadRequest('Can "Upgrade" only to "WebSocket".')
-
-        if 'upgrade' not in environ.get('HTTP_CONNECTION', '').lower():
-            #self.stream.close()
-            return HTTPBadRequest('"Connection" must be "Upgrade".')
-
-        # 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".
-
-        version = environ.get("HTTP_SEC_WEBSOCKET_VERSION")
-        if not version or version not in ('13', '8', '7'):
-            # dejo la opción de pyramid_socjs
-            return HTTPBadRequest('Unsupported WebSocket version.')
-
-            response.status = 426
-            response.headers = (
-                ('Sec-WebSocket-Version', '8'),
-            )
-            #self.stream.close()
-            return response
-
-        # check client handshake for validity
-        protocol = environ.get('SERVER_PROTOCOL', '')
-        if not protocol.startswith("HTTP/"):
-            return HTTPBadRequest('Protocol is not HTTP')
-
-        if not (environ.get('GATEWAY_INTERFACE', '').endswith('/1.1') or \
-                  protocol.endswith('/1.1')):
-            return HTTPBadRequest('HTTP/1.1 is required')
-
-        key = environ.get("HTTP_SEC_WEBSOCKET_KEY")
-        if not key or len(base64.b64decode(key)) != 16:
-            return HTTPBadRequest('HTTP_SEC_WEBSOCKET_KEY is invalid key')
-
-        fields = ("Host", "Sec-Websocket-Key", "Sec-Websocket-Version")
-        if not all(map(lambda f: self.request.headers.get(f), fields)):
-            return HTTPBadRequest('Missing/Invalid WebSocket headers')
-
-        #------------------------------#
-        #   Accept the connection
-        #------------------------------#
-        self.ws_connection = WebSocketProtocol13(self, self.auto_decode())
-        self._accept_connection()
-
-        response = WebsocketResponse(self.context, self.request)
-        return response
-
-    def _challenge_response(self):
-        sha1 = hashlib.sha1()
-        sha1.update(ginsfsm.escape.utf8(
-                self.request.headers.get("Sec-Websocket-Key")))
-        sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
-        return ginsfsm.escape.native_str(base64.b64encode(sha1.digest()))
-
-    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.select_subprotocol(subprotocols)
-            if selected:
-                assert selected in subprotocols
-                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % (
-                    selected)
-
-        self.stream.mt_send_data(ginsfsm.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"
-            "\r\n" % (self._challenge_response(), subprotocol_header)))
-
-        self.open()
-        #self._receive_frame()
-
-    def abort_connection(self):
-        self.ws_connection._abort()
-
-    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 isinstance(message, dict):
-            message = ginsfsm.escape.json_encode(message)
-        self.ws_connection.write_message(message, binary=binary)
-
-    def get_conn_info(self):
-        """Return `ConnectionInfo` object from current transport"""
-        return ConnectionInfo(
-            self.request.remote_addr,  # remote_ip
-            self.request.cookies,
-            self.request.params,  # arguments
-            self.request.headers,
-            self.request.path
-        )
-
-    def select_subprotocol(self, subprotocols):
-        """Invoked when a new WebSocket requests specific subprotocols.
-
-        ``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.
-        """
-        return None
-
-    def on_open(self):
-        """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`.
-        """
-        pass
-
-    def on_message(self, message):
-        """Handle incoming messages on the WebSocket
-
-        This method must be overridden.
-        """
-        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.
-        """
-        self.ws_connection.close()
-
-    def auto_decode(self):
-        """Override to disable automatic utf-8 decoding of the string messages.
-
-        If you will return False, your on_message handler will receive utf-8
-        encoded strings.
-
-        Useful for performance reasons - if your protocol is json based, most
-        of the python json decoders work with utf-8 encoded strings and if they
-        receive utf-16 as input, then will convert it back to utf-8
-        and then parse.
-        """
-        return True
-
-    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 async_callback(self, callback, *args, **kwargs):
-        """Wrap callbacks with this if they are used on asynchronous requests.
-
-        Catches exceptions properly and closes this WebSocket if an exception
-        is uncaught.  (Note that this is usually unnecessary thanks to
-        `tornado.stack_context`)
-        """
-        return self.ws_connection.async_callback(callback, *args, **kwargs)
-
-    def _not_supported(self, *args, **kwargs):
-        raise Exception("Method not supported for Web Sockets")
-
-    def on_connection_close(self):
-        if self.ws_connection:
-            self.ws_connection.on_connection_close()
-            self.ws_connection = None
-            self.on_close()
-
-
-"""
-for method in ["write", "redirect", "set_header", "send_error", "set_cookie",
-               "set_status", "flush", "finish"]:
-    setattr(WebSocketHandler, method, WebSocketHandler._not_supported)
-"""
-
-
-class WebSocketProtocol(object):
-    """Base class for WebSocket protocol versions.
-    """
-    def __init__(self, handler, auto_decode):
-        self.handler = handler
-        self.request = handler.request
-        self.stream = handler.stream
-        self.client_terminated = False
-        self.server_terminated = False
-
-        self._auto_decode = auto_decode
-
-    def async_callback(self, callback, *args, **kwargs):
-        """Wrap callbacks with this if they are used on asynchronous requests.
-
-        Catches exceptions properly and closes this WebSocket 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:
-                logging.error("Uncaught exception in %s",
-                              self.request.path, exc_info=True)
-                self._abort()
-
-        return wrapper
-
-    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
-
-
-STRUCT_BB = struct.Struct("BB")
-STRUCT_BBH = struct.Struct("!BBH")
-STRUCT_BBQ = struct.Struct("!BBQ")
-STRUCT_H = struct.Struct("!H")
-STRUCT_Q = struct.Struct("!Q")
-
-
-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.
-    """
-    def __init__(self, handler, auto_decode):
-        WebSocketProtocol.__init__(self, handler, auto_decode)
-        self._final_frame = False
-        self._frame_opcode = None
-        self._frame_mask = None
-        self._frame_length = None
-        self._fragmented_message_buffer = None
-        self._fragmented_message_opcode = None
-        self._waiting = None
-
-    def _write_frame(self, fin, opcode, data):
-        if fin:
-            finbit = opcode | 0x80
-        else:
-            finbit = opcode
-
-        l = len(data)
-        if l < 126:
-            frame = STRUCT_BB.pack(finbit, l)
-        elif l <= 0xFFFF:
-            frame = STRUCT_BBH.pack(finbit, 126, l)
-        else:
-            frame = STRUCT_BBQ.pack(finbit, 127, l)
-        frame += data
-        self.stream.mt_send_data(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 = ginsfsm.escape.utf8(message)
-        assert isinstance(message, bytes_type)
-        self._write_frame(True, opcode, message)
-
-    def _receive_frame(self):
-        self.stream.read_bytes(2, self._on_frame_start)
-
-    def _on_frame_start(self, data):
-        header, payloadlen = STRUCT_BB.unpack(data)
-        self._final_frame = header & 0x80
-        reserved_bits = header & 0x70
-        self._frame_opcode = header & 0xf
-        self._frame_opcode_is_control = self._frame_opcode & 0x8
-        if reserved_bits:
-            # client is using as-yet-undefined extensions; abort
-            self._abort()
-            return
-        if not (payloadlen & 0x80):
-            # Unmasked frame -> abort connection
-            self._abort()
-            return
-        payloadlen = payloadlen & 0x7f
-        if self._frame_opcode_is_control and payloadlen >= 126:
-            # control frames must have payload < 126
-            self._abort()
-            return
-        if payloadlen < 126:
-            self._frame_length = payloadlen
-            self.stream.read_bytes(4, self._on_masking_key)
-        elif payloadlen == 126:
-            self.stream.read_bytes(2, self._on_frame_length_16)
-        elif payloadlen == 127:
-            self.stream.read_bytes(8, self._on_frame_length_64)
-
-    def _on_frame_length_16(self, data):
-        self._frame_length = STRUCT_H.unpack(data)[0]
-        self.stream.read_bytes(4, self._on_masking_key)
-
-    def _on_frame_length_64(self, data):
-        self._frame_length = STRUCT_Q.unpack(data)[0]
-        self.stream.read_bytes(4, self._on_masking_key)
-
-    def _on_masking_key(self, data):
-        self._frame_mask = make_array(data)
-        self.stream.read_bytes(self._frame_length, self._on_frame_data)
-
-    def _on_frame_data(self, data):
-        unmasked = make_array(data)
-        mask = self._frame_mask
-        for i in xrange(len(data)):
-            unmasked[i] = unmasked[i] ^ mask[i % 4]
-
-        if self._frame_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 self._final_frame:
-                # control frames must not be fragmented
-                self._abort()
-                return
-            opcode = self._frame_opcode
-        elif self._frame_opcode == 0:  # continuation frame
-            if self._fragmented_message_buffer is None:
-                # nothing to continue
-                self._abort()
-                return
-            self._fragmented_message_buffer += unmasked
-            if self._final_frame:
-                opcode = self._fragmented_message_opcode
-                unmasked = 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 self._final_frame:
-                opcode = self._frame_opcode
-            else:
-                self._fragmented_message_opcode = self._frame_opcode
-                self._fragmented_message_buffer = unmasked
-
-        if self._final_frame:
-            self._handle_message(opcode, to_string(unmasked))
-
-        if not self.client_terminated:
-            self._receive_frame()
-
-    def _handle_message(self, opcode, data):
-        if self.client_terminated:
-            return
-
-        if opcode == 0x1:
-            # UTF-8 data
-            try:
-                if self._auto_decode:
-                    decoded = data.decode("utf-8")
-                else:
-                    decoded = data
-            except UnicodeDecodeError:
-                self._abort()
-                return
-            self.async_callback(self.handler.on_message)(decoded)
-        elif opcode == 0x2:
-            # Binary data
-            self.async_callback(self.handler.on_message)(data)
-        elif opcode == 0x8:
-            # Close
-            self.client_terminated = True
-            self.close()
-        elif opcode == 0x9:
-            # Ping
-            self._write_frame(True, 0xA, data)
-        elif opcode == 0xA:
-            # Pong
-            pass
-        else:
-            self._abort()
-
-    def close(self):
-        """Closes the WebSocket connection."""
-        if not self.server_terminated:
-            if not self.stream.closed():
-                self._write_frame(True, 0x8, b(""))
-            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(
-                time.time() + 5, self._abort)