Commits

Ginés Martínez Sánchez  committed 79d06b3 Draft

developing websocket protocol

  • Participants
  • Parent commits d260ee5

Comments (0)

Files changed (6)

File ginsfsm/escape.py

+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""Escaping/unescaping methods for HTML, JSON, URLs, and others.
+
+Also includes a few other miscellaneous string manipulation functions that
+have crept in over time.
+"""
+
+from __future__ import absolute_import, division, with_statement
+
+import htmlentitydefs
+import re
+import sys
+import urllib
+
+# Python3 compatibility:  On python2.5, introduce the bytes alias from 2.6
+try:
+    bytes
+except Exception:
+    bytes = str
+
+try:
+    from urlparse import parse_qs  # Python 2.6+
+except ImportError:
+    from cgi import parse_qs
+
+# json module is in the standard library as of python 2.6; fall back to
+# simplejson if present for older versions.
+try:
+    import json
+    assert hasattr(json, "loads") and hasattr(json, "dumps")
+    _json_decode = json.loads
+    _json_encode = json.dumps
+except Exception:
+    try:
+        import simplejson
+        _json_decode = lambda s: simplejson.loads(_unicode(s))
+        _json_encode = lambda v: simplejson.dumps(v)
+    except ImportError:
+        try:
+            # For Google AppEngine
+            from django.utils import simplejson
+            _json_decode = lambda s: simplejson.loads(_unicode(s))
+            _json_encode = lambda v: simplejson.dumps(v)
+        except ImportError:
+            def _json_decode(s):
+                raise NotImplementedError(
+                    "A JSON parser is required, e.g., simplejson at "
+                    "http://pypi.python.org/pypi/simplejson/")
+            _json_encode = _json_decode
+
+
+_XHTML_ESCAPE_RE = re.compile('[&<>"]')
+_XHTML_ESCAPE_DICT = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}
+
+
+def xhtml_escape(value):
+    """Escapes a string so it is valid within XML or XHTML."""
+    return _XHTML_ESCAPE_RE.sub(
+        lambda match: _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value))
+
+
+def xhtml_unescape(value):
+    """Un-escapes an XML-escaped string."""
+    return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
+
+
+def json_encode(value):
+    """JSON-encodes the given Python object."""
+    # JSON permits but does not require forward slashes to be escaped.
+    # This is useful when json data is emitted in a <script> tag
+    # in HTML, as it prevents </script> tags from prematurely terminating
+    # the javscript.  Some json libraries do this escaping by default,
+    # although python's standard library does not, so we do it here.
+    # http://stackoverflow.com/questions/1580647/json-why-are-forward-slashes-escaped
+    return _json_encode(recursive_unicode(value)).replace("</", "<\\/")
+
+
+def json_decode(value):
+    """Returns Python objects for the given JSON string."""
+    return _json_decode(to_basestring(value))
+
+
+def squeeze(value):
+    """Replace all sequences of whitespace chars with a single space."""
+    return re.sub(r"[\x00-\x20]+", " ", value).strip()
+
+
+def url_escape(value):
+    """Returns a valid URL-encoded version of the given value."""
+    return urllib.quote_plus(utf8(value))
+
+# python 3 changed things around enough that we need two separate
+# implementations of url_unescape.  We also need our own implementation
+# of parse_qs since python 3's version insists on decoding everything.
+if sys.version_info[0] < 3:
+    def url_unescape(value, encoding='utf-8'):
+        """Decodes the given value from a URL.
+
+        The argument may be either a byte or unicode string.
+
+        If encoding is None, the result will be a byte string.  Otherwise,
+        the result is a unicode string in the specified encoding.
+        """
+        if encoding is None:
+            return urllib.unquote_plus(utf8(value))
+        else:
+            return unicode(urllib.unquote_plus(utf8(value)), encoding)
+
+    parse_qs_bytes = parse_qs
+else:
+    def url_unescape(value, encoding='utf-8'):
+        """Decodes the given value from a URL.
+
+        The argument may be either a byte or unicode string.
+
+        If encoding is None, the result will be a byte string.  Otherwise,
+        the result is a unicode string in the specified encoding.
+        """
+        if encoding is None:
+            return urllib.parse.unquote_to_bytes(value)
+        else:
+            return urllib.unquote_plus(to_basestring(value), encoding=encoding)
+
+    def parse_qs_bytes(qs, keep_blank_values=False, strict_parsing=False):
+        """Parses a query string like urlparse.parse_qs, but returns the
+        values as byte strings.
+
+        Keys still become type str (interpreted as latin1 in python3!)
+        because it's too painful to keep them as byte strings in
+        python3 and in practice they're nearly always ascii anyway.
+        """
+        # This is gross, but python3 doesn't give us another way.
+        # Latin1 is the universal donor of character encodings.
+        result = parse_qs(qs, keep_blank_values, strict_parsing,
+                          encoding='latin1', errors='strict')
+        encoded = {}
+        for k, v in result.iteritems():
+            encoded[k] = [i.encode('latin1') for i in v]
+        return encoded
+
+
+_UTF8_TYPES = (bytes, type(None))
+
+
+def utf8(value):
+    """Converts a string argument to a byte string.
+
+    If the argument is already a byte string or None, it is returned unchanged.
+    Otherwise it must be a unicode string and is encoded as utf8.
+    """
+    if isinstance(value, _UTF8_TYPES):
+        return value
+    assert isinstance(value, unicode)
+    return value.encode("utf-8")
+
+_TO_UNICODE_TYPES = (unicode, type(None))
+
+
+def to_unicode(value):
+    """Converts a string argument to a unicode string.
+
+    If the argument is already a unicode string or None, it is returned
+    unchanged.  Otherwise it must be a byte string and is decoded as utf8.
+    """
+    if isinstance(value, _TO_UNICODE_TYPES):
+        return value
+    assert isinstance(value, bytes)
+    return value.decode("utf-8")
+
+# to_unicode was previously named _unicode not because it was private,
+# but to avoid conflicts with the built-in unicode() function/type
+_unicode = to_unicode
+
+# When dealing with the standard library across python 2 and 3 it is
+# sometimes useful to have a direct conversion to the native string type
+if str is unicode:
+    native_str = to_unicode
+else:
+    native_str = utf8
+
+_BASESTRING_TYPES = (basestring, type(None))
+
+
+def to_basestring(value):
+    """Converts a string argument to a subclass of basestring.
+
+    In python2, byte and unicode strings are mostly interchangeable,
+    so functions that deal with a user-supplied argument in combination
+    with ascii string constants can use either and should return the type
+    the user supplied.  In python3, the two types are not interchangeable,
+    so this method is needed to convert byte strings to unicode.
+    """
+    if isinstance(value, _BASESTRING_TYPES):
+        return value
+    assert isinstance(value, bytes)
+    return value.decode("utf-8")
+
+
+def recursive_unicode(obj):
+    """Walks a simple data structure, converting byte strings to unicode.
+
+    Supports lists, tuples, and dictionaries.
+    """
+    if isinstance(obj, dict):
+        return dict(
+            (recursive_unicode(k), recursive_unicode(v)) \
+                for (k, v) in obj.iteritems()
+        )
+    elif isinstance(obj, list):
+        return list(recursive_unicode(i) for i in obj)
+    elif isinstance(obj, tuple):
+        return tuple(recursive_unicode(i) for i in obj)
+    elif isinstance(obj, bytes):
+        return to_unicode(obj)
+    else:
+        return obj
+
+# I originally used the regex from
+# http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+# but it gets all exponential on certain patterns (such as too many trailing
+# dots), causing the regex matcher to never return.
+# This regex should avoid those problems.
+_URL_RE = re.compile(
+    ur"""\b((?:([\w-]+):(/{1,3})|www[.])(?:(?:(?:[^\s&()]|&amp;|&quot;)*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&amp;|&quot;)*\)))+)"""
+)
+
+
+def linkify(text, shorten=False, extra_params="",
+            require_protocol=False, permitted_protocols=["http", "https"]):
+    """Converts plain text into HTML with links.
+
+    For example: ``linkify("Hello http://tornadoweb.org!")`` would return
+    ``Hello <a href="http://tornadoweb.org">http://tornadoweb.org</a>!``
+
+    Parameters:
+
+    shorten: Long urls will be shortened for display.
+
+    extra_params: Extra text to include in the link tag, or a callable
+        taking the link as an argument and returning the extra text
+        e.g. ``linkify(text, extra_params='rel="nofollow" class="external"')``,
+        or::
+
+            def extra_params_cb(url):
+                if url.startswith("http://example.com"):
+                    return 'class="internal"'
+                else:
+                    return 'class="external" rel="nofollow"'
+            linkify(text, extra_params=extra_params_cb)
+
+    require_protocol: Only linkify urls which include a protocol. If this is
+        False, urls such as www.facebook.com will also be linkified.
+
+    permitted_protocols: List (or set) of protocols which should be linkified,
+        e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]).
+        It is very unsafe to include protocols such as "javascript".
+    """
+    if extra_params and not callable(extra_params):
+        extra_params = " " + extra_params.strip()
+
+    def make_link(m):
+        url = m.group(1)
+        proto = m.group(2)
+        if require_protocol and not proto:
+            return url  # not protocol, no linkify
+
+        if proto and proto not in permitted_protocols:
+            return url  # bad protocol, no linkify
+
+        href = m.group(1)
+        if not proto:
+            href = "http://" + href   # no proto specified, use http
+
+        if callable(extra_params):
+            params = " " + extra_params(href).strip()
+        else:
+            params = extra_params
+
+        # clip long urls. max_len is just an approximation
+        max_len = 30
+        if shorten and len(url) > max_len:
+            before_clip = url
+            if proto:
+                proto_len = len(proto) + 1 + len(m.group(3) or "")  # +1 for :
+            else:
+                proto_len = 0
+
+            parts = url[proto_len:].split("/")
+            if len(parts) > 1:
+                # Grab the whole host part plus the first bit of the path
+                # The path is usually not that interesting once shortened
+                # (no more slug, etc), so it really just provides a little
+                # extra indication of shortening.
+                url = url[:proto_len] + parts[0] + "/" + \
+                        parts[1][:8].split('?')[0].split('.')[0]
+
+            if len(url) > max_len * 1.5:  # still too long
+                url = url[:max_len]
+
+            if url != before_clip:
+                amp = url.rfind('&')
+                # avoid splitting html char entities
+                if amp > max_len - 5:
+                    url = url[:amp]
+                url += "..."
+
+                if len(url) >= len(before_clip):
+                    url = before_clip
+                else:
+                    # full url is visible on mouse-over (for those who don't
+                    # have a status bar, such as Safari by default)
+                    params += ' title="%s"' % href
+
+        return u'<a href="%s"%s>%s</a>' % (href, params, url)
+
+    # First HTML-escape so that our strings are all safe.
+    # The regex is modified to avoid character entites other than &amp; so
+    # that we won't pick up &quot;, etc.
+    text = _unicode(xhtml_escape(text))
+    return _URL_RE.sub(make_link, text)
+
+
+def _convert_entity(m):
+    if m.group(1) == "#":
+        try:
+            return unichr(int(m.group(2)))
+        except ValueError:
+            return "&#%s;" % m.group(2)
+    try:
+        return _HTML_UNICODE_MAP[m.group(2)]
+    except KeyError:
+        return "&%s;" % m.group(2)
+
+
+def _build_unicode_map():
+    unicode_map = {}
+    for name, value in htmlentitydefs.name2codepoint.iteritems():
+        unicode_map[name] = unichr(value)
+    return unicode_map
+
+_HTML_UNICODE_MAP = _build_unicode_map()

File ginsfsm/protocols/sockjs/server/c_rawwebsocket.py

-# -*- coding: utf-8 -*-
-"""
-    HtmlFile transport implementation.
-"""
-from pyramid.view import view_config
-
-from ginsfsm.protocols.sockjs.server.proto import json_encode
-from ginsfsm.gobj import GObj
-from ginsfsm.protocols.sockjs.server.basehandler import PreflightHandler
-
-#----------------------------------------------------------------#
-#                   GRawWebsocket GClass
-#                   /rawwebsocket
-#----------------------------------------------------------------#
-GRAWWEBSOCKET_GCONFIG = {
-    'sockjs_server': [None, None, 0, None, ""],
-}
-
-
-class GRawWebsocket(GObj):
-    """  GRawWebsocket GObj.
-    """
-    def __init__(self):
-        GObj.__init__(self, {}, GRAWWEBSOCKET_GCONFIG)
-
-    def start_up(self):
-        """ Initialization zone.
-        """
-
-
-#----------------------------------------------------------------#
-#                   GRawWebsocket Views
-#                   /rawwebsocket
-#----------------------------------------------------------------#
-
-@view_config(
-    context=GRawWebsocket,
-    name='',
-    attr='options',
-    request_method='OPTIONS',
-)
-@view_config(
-    context=GRawWebsocket,
-    name='',
-    attr='get',
-    request_method='GET',
-)
-class RawWebsocketTransport(PreflightHandler):
-    name = 'rawwebsocket'
-
-    def __init__(self, context, request):
-        super(RawWebsocketTransport, self).__init__(
-            context,
-            request,
-            True  # Asynchronous response!
-        )
-        self.session = None
-        self.active = True
-        self.amount_limit = self.context.sockjs_server.response_limit
-
-    def get(self):
-        response = self.response
-
-        session_id = self.sid = self.context.parent.re_matched_name
-
-        # Start response
-        self.preflight()
-        self.handle_session_cookie()
-        self.disable_cache()
-
-        response.content_type = 'text/html'
-        response.charset = 'UTF-8'
-
-        # Grab callback parameter
-        self.callback = self.request.GET.get('c', None)
-        if self.callback is None:
-            self.set_body('"callback" parameter required')
-            self.set_status(500)
-            return response
-
-        # TODO: Fix me - use parameter
-        self.write(HTMLFILE_HEAD % self.callback)
-        self.flush()
-
-        # Get or create session without starting heartbeat
-        session = self.context.sockjs_server.get_session(session_id)
-        if session is None:
-            session = self.context.sockjs_server.create_session(session_id)
-        if session is None:
-            # close the session in the next cycle.
-            #self.context.gaplic.add_callback(self.xxx)
-            return response  # how inform of the error? headers has been sent.
-
-        # Try to attach to the session
-        if not session.set_handler(self, False):
-            # close the session in the next cycle.
-            #self.context.gaplic.add_callback(self.xxx)
-            return response  # how inform of the error? headers has been sent.
-        self.session = session
-        # Verify if session is properly opened
-        session.verify_state()
-
-        session.flush()
-
-        return response
-
-    def send_pack(self, message, binary=False):
-        if binary:
-            raise Exception('binary not supported for HtmlFileTransport')
-
-        # TODO: Just do escaping
-        msg = '<script>\np(%s);\n</script>\r\n' % json_encode(message)
-
-        #self.active = False  # TODO si chequeo active fallan los tests
-
-        try:
-            self.notify_sent(len(msg))
-            self.write(msg)
-            self.flush(callback=self.send_complete)
-        except:
-            # If connection dropped, make sure we close offending session
-            # instead of propagating error all way up.
-            if self.session:  # detach session
-                self.session.delayed_close()
-                self.session.remove_handler(self)
-                self.session = None
-
-    def send_complete(self):
-        """
-            Verify if connection should be closed based on amount of data
-            that was sent.
-        """
-        self.active = True
-
-        if self.should_finish():
-            if self.session:  # detach session
-                self.session.remove_handler(self)
-                self.session = None
-            self.context.gaplic.add_callback(self.safe_finish)
-        else:
-            if self.session:
-                self.session.flush()
-
-    def notify_sent(self, data_len):
-        """
-            Update amount of data sent
-        """
-        self.amount_limit -= data_len
-
-    def should_finish(self):
-        """
-            Check if transport should close long running connection after
-            sending X bytes to the client.
-
-            `data_len`
-                Amount of data that was sent
-        """
-        if self.amount_limit <= 0:
-            return True
-
-        return False
-
-    def session_closed(self):
-        """Called by the session when it was closed"""
-        if self.session:  # detach session
-            self.session.remove_handler(self)
-            self.session = None
-        self.context.gaplic.add_callback(self.safe_finish)

File ginsfsm/protocols/sockjs/server/c_sockjs_server.py

     GInfo,
     GIFrame,
 )
-from ginsfsm.protocols.sockjs.server.c_rawwebsocket import GRawWebsocket
+from ginsfsm.protocols.sockjs.server.c_websocket import GRawWebsocket
 from ginsfsm.protocols.sockjs.server import session, sessioncontainer
 from ginsfsm.protocols.sockjs.server import stats
 from ginsfsm.protocols.sockjs.server.c_session import GSockjsSession

File ginsfsm/protocols/sockjs/server/c_websocket.py

 # -*- coding: utf-8 -*-
 """
-    HtmlFile transport implementation.
+    Websocket transport implementation
 """
+import logging
+import random
+
 from pyramid.view import view_config
 
-from ginsfsm.protocols.sockjs.server.proto import json_encode
+from ginsfsm.protocols.sockjs.server.proto import json_decode
 from ginsfsm.gobj import GObj
 from ginsfsm.protocols.sockjs.server.basehandler import PreflightHandler
+from ginsfsm.protocols.sockjs.server.session import BaseSession
+
 
 #----------------------------------------------------------------#
 #                   GWebsocket GClass
 @view_config(
     context=GWebsocket,
     name='',
-    attr='options',
-    request_method='OPTIONS',
-)
-@view_config(
-    context=GWebsocket,
-    name='',
     attr='get',
     request_method='GET',
 )
         )
         self.session = None
         self.active = True
-        self.amount_limit = self.context.sockjs_server.response_limit
+
+    def open(self, session_id):
+        # Stats
+        self.server.stats.on_conn_opened()
+
+        # Disable nagle
+        #if self.server.settings['disable_nagle']:
+        #    self.stream.socket.setsockopt(
+        #        socket.SOL_TCP, socket.TCP_NODELAY, 1)
+
+        # Handle session
+        self.session = self.server.create_session(session_id, register=False)
+
+        if not self.session.set_handler(self):
+            self.close()
+            return
+
+        self.session.verify_state()
+
+        if self.session:
+            self.session.flush()
+
+    def _detach(self):
+        if self.session is not None:
+            self.session.remove_handler(self)
+            self.session = None
+
+    def on_message(self, message):
+        # SockJS requires that empty messages should be ignored
+        if not message or not self.session:
+            return
+
+        try:
+            msg = json_decode(message)
+
+            if isinstance(msg, list):
+                self.session.on_messages(msg)
+            else:
+                self.session.on_messages((msg,))
+        except Exception:
+            logging.exception('WebSocket')
+
+            # Close session on exception
+            #self.session.close()
+
+            # Close running connection
+            self.abort_connection()
+
+    def on_close(self):
+        # Close session if websocket connection was closed
+        if self.session is not None:
+            # Stats
+            self.server.stats.on_conn_closed()
+
+            # Detach before closing session
+            session = self.session
+            self._detach()
+            session.close()
+
+    def send_pack(self, message, binary=False):
+        # Send message
+        try:
+            self.write_message(message, binary)
+        except IOError:
+            self.server.io_loop.add_callback(self.on_close)
+
+    def session_closed(self):
+        # If session was closed by the application, terminate websocket
+        # connection as well.
+        try:
+            self.close()
+        except IOError:
+            pass
+        finally:
+            self._detach()
+
+    # Websocket overrides
+    def allow_draft76(self):
+        return True
+
+    def auto_decode(self):
+        return False
+
+
+#----------------------------------------------------------------#
+#                   RawWebsocket Session
+#----------------------------------------------------------------#
+class RawSession(BaseSession):
+    """ Raw session without any sockjs protocol encoding/decoding.
+        Simply works as a proxy between `SockJSConnection` class
+        and `RawWebSocketTransport`.
+    """
+    def send_message(self, msg, stats=True, binary=False):
+        self.handler.send_pack(msg, binary)
+
+    def on_message(self, msg):
+        self.conn.on_message(msg)
+
+
+#----------------------------------------------------------------#
+#                   GRawWebsocket GClass
+#                   /rawwebsocket
+#----------------------------------------------------------------#
+GRAWWEBSOCKET_GCONFIG = {
+    'sockjs_server': [None, None, 0, None, ""],
+}
+
+
+class GRawWebsocket(GObj):
+    """  GRawWebsocket GObj.
+    """
+    def __init__(self):
+        GObj.__init__(self, {}, GRAWWEBSOCKET_GCONFIG)
+
+    def start_up(self):
+        """ Initialization zone.
+        """
+
+
+#----------------------------------------------------------------#
+#                   GRawWebsocket Views
+#                   /rawwebsocket
+#----------------------------------------------------------------#
+@view_config(
+    context=GRawWebsocket,
+    name='',
+    attr='get',
+    request_method='GET',
+)
+class RawWebsocketTransport(PreflightHandler):
+    name = 'rawwebsocket'
+
+    def __init__(self, context, request):
+        super(RawWebsocketTransport, self).__init__(
+            context,
+            request,
+            True  # Asynchronous response!
+        )
+        self.session = None
+        self.active = True
 
     def get(self):
+        # session
         response = self.response
+        session_id = self.sid = '%0.9d' % random.randint(1, 2147483647)
 
-        session_id = self.sid = self.context.parent.re_matched_name
+        manager = self.session_manager
 
-        # Start response
-        self.preflight()
-        self.handle_session_cookie()
-        self.disable_cache()
+        sid = '%0.9d' % random.randint(1, 2147483647)
+        #if self.per_user:
+        #    sid = (authenticated_userid(request), sid)
 
-        response.content_type = 'text/html'
-        response.charset = 'UTF-8'
+        session = manager.get(sid, True, request=request)
+        request.environ['wsgi.sockjs_session'] = session
 
-        # Grab callback parameter
-        self.callback = self.request.GET.get('c', None)
-        if self.callback is None:
-            self.set_body('"callback" parameter required')
-            self.set_status(500)
-            return response
+        # websocket
+        if 'HTTP_ORIGIN' in request.environ:
+            return HTTPNotFound()
 
-        # TODO: Fix me - use parameter
-        self.flush()
+        res = init_websocket(request)
+        if res is not None:
+            return res
 
-        # Get or create session without starting heartbeat
-        session = self.context.sockjs_server.get_session(session_id)
-        if session is None:
-            session = self.context.sockjs_server.create_session(session_id)
-        if session is None:
-            # close the session in the next cycle.
-            #self.context.gaplic.add_callback(self.xxx)
-            return response  # how inform of the error? headers has been sent.
+        return RawWebSocketTransport(session, request)
 
-        # Try to attach to the session
-        if not session.set_handler(self, False):
-            # close the session in the next cycle.
-            #self.context.gaplic.add_callback(self.xxx)
-            return response  # how inform of the error? headers has been sent.
-        self.session = session
-        # Verify if session is properly opened
-        session.verify_state()
 
-        session.flush()
+    def open(self):
+        # Stats
+        self.server.stats.on_conn_opened()
 
-        return response
+        # Disable nagle if needed
+        #if self.server.settings['disable_nagle']:
+        #    self.stream.socket.setsockopt(
+        #        socket.SOL_TCP, socket.TCP_NODELAY, 1)
+
+        # Create and attach to session
+        self.session = RawSession(
+            self.server.get_connection_class(),
+            self.server
+        )
+        self.session.set_handler(self)
+        self.session.verify_state()
+
+    def _detach(self):
+        if self.session is not None:
+            self.session.remove_handler(self)
+            self.session = None
+
+    def on_message(self, message):
+        # SockJS requires that empty messages should be ignored
+        if not message or not self.session:
+            return
+
+        try:
+            self.session.on_message(message)
+        except Exception:
+            logging.exception('RawWebSocket')
+
+            # Close running connection
+            self._detach()
+            self.abort_connection()
+
+    def on_close(self):
+        # Close session if websocket connection was closed
+        if self.session is not None:
+            # Stats
+            self.server.stats.on_conn_closed()
+
+            session = self.session
+            self._detach()
+            session.close()
 
     def send_pack(self, message, binary=False):
-        if binary:
-            raise Exception('binary not supported for HtmlFileTransport')
-
-        # TODO: Just do escaping
-        msg = '<script>\np(%s);\n</script>\r\n' % json_encode(message)
-
-        #self.active = False  # TODO si chequeo active fallan los tests
-
+        # Send message
         try:
-            self.notify_sent(len(msg))
-            self.write(msg)
-            self.flush(callback=self.send_complete)
-        except:
-            # If connection dropped, make sure we close offending session
-            # instead of propagating error all way up.
-            if self.session:  # detach session
-                self.session.delayed_close()
-                self.session.remove_handler(self)
-                self.session = None
-
-    def send_complete(self):
-        """
-            Verify if connection should be closed based on amount of data
-            that was sent.
-        """
-        self.active = True
-
-        if self.should_finish():
-            if self.session:  # detach session
-                self.session.remove_handler(self)
-                self.session = None
-            self.context.gaplic.add_callback(self.safe_finish)
-        else:
-            if self.session:
-                self.session.flush()
-
-    def notify_sent(self, data_len):
-        """
-            Update amount of data sent
-        """
-        self.amount_limit -= data_len
-
-    def should_finish(self):
-        """
-            Check if transport should close long running connection after
-            sending X bytes to the client.
-
-            `data_len`
-                Amount of data that was sent
-        """
-        if self.amount_limit <= 0:
-            return True
-
-        return False
+            self.write_message(message, binary)
+        except IOError:
+            self.server.io_loop.add_callback(self.on_close)
 
     def session_closed(self):
-        """Called by the session when it was closed"""
-        if self.session:  # detach session
-            self.session.remove_handler(self)
-            self.session = None
-        self.context.gaplic.add_callback(self.safe_finish)
+        try:
+            self.close()
+        except IOError:
+            pass
+        finally:
+            self._detach()
+
+    # Websocket overrides
+    def allow_draft76(self):
+        return True

File ginsfsm/protocols/sockjs/server/websocket.py

 """
     websocket
 
+    This module is a modified version of the WebSocketHandler from
+    tornado.sockjs adapted to ginsfsm.
+
     This module contains modified version of the WebSocketHandler from
     Tornado with some performance fixes and behavior changes for Hybi10
     protocol.
 import struct
 import time
 import base64
-import tornado.escape
-import tornado.web
+import ginsfsm.escape
+#import tornado.web
 
-# TODO remove
-from tornado import stack_context
-from tornado.util import bytes_type, b
+
+# 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:
 
         # Websocket only supports GET method
         if self.request.method != "GET":
-            self.stream.write(tornado.escape.utf8(
+            self.stream.write(ginsfsm.escape.utf8(
                 "HTTP/1.1 405 Method Not Allowed\r\n"
                 "Allow: GET\r\n"
                 "Connection: Close\r\n"
 
         # Upgrade header should be present and should be equal to WebSocket
         if self.request.headers.get("Upgrade", "").lower() != "websocket":
-            self.stream.write(tornado.escape.utf8(
+            self.stream.write(ginsfsm.escape.utf8(
                 "HTTP/1.1 400 Bad Request\r\n"
                 "Connection: Close\r\n"
                 "\r\n"
             self.stream.close()
             return
 
-        # Connection header should be upgrade. Some proxy servers/load balancers
-        # might mess with it.
+        # 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(","))
+        connection = map(
+            lambda s: s.strip().lower(),
+            headers.get("Connection", "").split(",")
+        )
         if "upgrade" not in connection:
-            self.stream.write(tornado.escape.utf8(
+            self.stream.write(ginsfsm.escape.utf8(
                 "HTTP/1.1 400 Bad Request\r\n"
                 "Connection: Close\r\n"
                 "\r\n"
         # 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 self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
+        if self.request.headers.get(
+                "Sec-WebSocket-Version") in ("7", "8", "13"):
             self.ws_connection = WebSocketProtocol13(self, self.auto_decode())
             self.ws_connection.accept_connection()
         elif (self.allow_draft76() and
             self.ws_connection = WebSocketProtocol76(self, self.auto_decode())
             self.ws_connection.accept_connection()
         else:
-            self.stream.write(tornado.escape.utf8(
+            self.stream.write(ginsfsm.escape.utf8(
                 "HTTP/1.1 426 Upgrade Required\r\n"
                 "Sec-WebSocket-Version: 8\r\n\r\n"))
             self.stream.close()
         is allowed.
         """
         if isinstance(message, dict):
-            message = tornado.escape.json_encode(message)
+            message = ginsfsm.escape.json_encode(message)
         self.ws_connection.write_message(message, binary=binary)
 
     def select_subprotocol(self, subprotocols):
 
         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.
+        receive utf-16 as input, then will convert it back to utf-8
+        and then parse.
         """
         return True
 
             selected = self.handler.select_subprotocol([subprotocol])
             if selected:
                 assert selected == subprotocol
-                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+                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(
+        self.stream.write(ginsfsm.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"
+            "Server: ginsfsm/%(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,
+                    version=ginsfsm.__version__,
                     origin=self.request.headers["Origin"],
                     scheme=scheme,
                     host=self.request.host,
 
     def _write_response(self, challenge):
         self.stream.write(challenge)
-        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+        self.async_callback(self.handler.open)(
+            *self.handler.open_args,
+            **self.handler.open_kwargs
+        )
         self._receive_message()
 
     def _handle_websocket_headers(self):
 
     def _challenge_response(self):
         sha1 = hashlib.sha1()
-        sha1.update(tornado.escape.utf8(
+        sha1.update(ginsfsm.escape.utf8(
                 self.request.headers.get("Sec-Websocket-Key")))
-        sha1.update(b("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) # Magic value
-        return tornado.escape.native_str(base64.b64encode(sha1.digest()))
+        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 = ''
             selected = self.handler.select_subprotocol(subprotocols)
             if selected:
                 assert selected in subprotocols
-                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % selected
+                subprotocol_header = "Sec-WebSocket-Protocol: %s\r\n" % (
+                    selected)
 
-        self.stream.write(tornado.escape.utf8(
+        self.stream.write(ginsfsm.escape.utf8(
             "HTTP/1.1 101 Switching Protocols\r\n"
             "Upgrade: websocket\r\n"
             "Connection: Upgrade\r\n"
             "%s"
             "\r\n" % (self._challenge_response(), subprotocol_header)))
 
-        self.async_callback(self.handler.open)(*self.handler.open_args, **self.handler.open_kwargs)
+        self.async_callback(self.handler.open)(
+            *self.handler.open_args,
+            **self.handler.open_kwargs
+        )
         self._receive_frame()
 
     def _write_frame(self, fin, opcode, data):
             opcode = 0x2
         else:
             opcode = 0x1
-        message = tornado.escape.utf8(message)
+        message = ginsfsm.escape.utf8(message)
         assert isinstance(message, bytes_type)
         self._write_frame(True, opcode, message)
 

File ginsfsm/protocols/wsgi/webob/websocket_response.py

+# -*- encoding: utf-8 -*-
+"""
+    WebsocketResponse -  an asynchronous Websocket WebOb Response over ginsfsm.
+"""
+import logging
+
+from ginsfsm.protocols.http.server.c_http_clisrv import ResponseInterrupt
+
+try:
+    from pyramid.response import Response
+except ImportError:  # pragma: no cover
+    try:
+        from webob import Response
+    except ImportError:  # pragma: no cover
+        raise Exception('async_response is depending of Pyramid or WebOb')
+
+
+class StopStreaming(StopIteration):
+    """ Connection has been disconnected. """
+
+
+class WebsocketResponse(Response):
+    def __init__(self, **kw):
+        super(WebsocketResponse, self).__init__(**kw)
+        self.ginsfsm_channel = None
+
+        del self.app_iter  # Important trick to remove Content-Length.
+        # WARNING: you cannot use body, because it sets Content-Length.
+
+    def __call__(self, environ, start_response):
+        "Override WSGI call"
+        self.ginsfsm_channel = environ['ginsfsm.channel']
+
+        if environ['wsgi.websocket_version'] == 'hixie-76':
+            part1, part2, socket = environ['wsgi.hixie-keys']
+
+            towrite = [
+                'HTTP/1.1 %s\r\n' % self.status,
+                'Date: %s\r\n' % format_date_time(time.time())]
+
+            for header in self._abs_headerlist(environ):
+                towrite.append("%s: %s\r\n" % header)
+
+            towrite.append("\r\n")
+            socket.sendall(''.join(towrite))
+
+            key3 = environ['wsgi.input'].read(8)
+            if not key3:
+                key3 = environ['wsgi.input'].rfile.read(8)
+
+            socket.sendall(
+                md5(struct.pack("!II", part1, part2) + key3).digest())
+        else:
+            write = start_response(
+                self.status, self._abs_headerlist(environ))
+            write(self.body)
+
+        try:
+            self.session.acquire(self.request)
+        except: # should use specific exception
+            self.websocket.send('o')
+            self.websocket.send(
+                close_frame(2010, "Another connection still open", '\n'))
+            self.websocket.close()
+            return StopStreaming()
+
+        if self.open():
+            gevent.joinall((gevent.spawn(self.send),
+                            gevent.spawn(self.receive)))
+        raise StopStreaming()
+
+        #
+        start_response(self.status, self._abs_headerlist(environ))
+
+        # send the current body to the network
+        for chunk in self.app_iter:
+            self.write(chunk)
+            self.flush()
+        del self.app_iter
+
+        raise ResponseInterrupt()
+
+    def write(self, data):
+        """ Write data to http channel.
+        """
+        if self.ginsfsm_channel:
+            self.ginsfsm_channel.write(data)
+        else:
+            # write in the sync response by now.
+            # in wsgi response call, all app_iter will be flush.
+            super(WebsocketResponse, self).write(data)
+
+    def flush(self, callback=None):
+        """Flushes the current output buffer to the network.
+
+        The ``callback`` argument, if given, can be used for flow control:
+        it will be run when all flushed data has been written to the socket.
+        Note that only one flush callback can be outstanding at a time;
+        if another flush occurs before the previous flush's callback
+        has been run, the previous callback will be discarded.
+        """
+        if self.ginsfsm_channel:
+            self.ginsfsm_channel.flush(callback)
+        else:
+            logging.error('ERROR async Flush before set ginsfsm_channel')
+
+    def finish(self):
+        """Finishes this response, ending the HTTP request."""
+        if self.ginsfsm_channel:
+            self.ginsfsm_channel.finish()
+        else:
+            logging.error('ERROR async FINISH before set ginsfsm_channel')