Commits

Jeffrey Gelens  committed b62a8d0 Merge

Merge develop branch to default branch in order to prevent confusion for Bitbucket people.

  • Participants
  • Parent commits ac89f5b, 8d92da1

Comments (0)

Files changed (16)

 syntax: glob
 
-.pyc
+*.pyc
+*.sw?
+gevent_websocket.egg-info/*
 tests/testresults.sqlite3
 577471fa42950b37578d56741fb510fe53a088de 0.2.0
 51bdf2cbcb708c6b188961cbec63b302feb731e4 0.2.1
 15e3a7e3ea3934134bbd2941d3f154d512f8438a 0.2.2
+b5da9e1da34ca8b558ee3a4f76dcedc1e4f7e7f3 0.2.3
+5f81c2ecfce59625cc1506ad96c7e12905abe7cc 0.3.0
+8360106dffe70b7da7f7ec0d67e1b9e666dced0c v0.3.1
+This Websocket library for Gevent is written and maintained by
+
+  Jeffrey Gelens <jeffrey at noppo.pro>
+
+
+Contributors:
+
+  Denis Bilenko
+  Lon Ingram
+Copyright (c) 2012, Noppo (Jeffrey Gelens) <http://www.noppo.pro/>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list
+of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+Neither the name of the Noppo nor the names of its contributors may be
+used to endorse or promote products derived from this software without specific
+prior written permission.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+================
+gevent-websocket
+================
+
+`gevent-websocket`_ is a websocket library for the gevent_ networking library
+written written and maintained by `Jeffrey Gelens`_ It is licensed under the BSD license.
+
+Installation
+------------
+
+Install Python 2.5 or newer and Gevent and its dependencies. The latest release
+can be download from PyPi_ or by cloning the repository_ and running::
+
+    $ python setup.py install
+
+The easiest way to install gevent-websocket is directly from PyPi_ using pip or
+setuptools by running the commands below::
+
+    $ pip install gevent-websocket
+
+or::
+
+    $ easy_install gevent-websocket
+
+This also installs the dependencies automatically.
+
+
+Usage
+-----
+
+Gevent Server
+^^^^^^^^^^^^^
+
+At the moment gevent-websocket has one handler based on the Pywsgi gevent
+Hook up the WebSocketHandler to the Pywsgi Server by setting the `handler_class`
+when creating the server instance.
+
+::
+
+    from gevent import pywsgi
+    from geventwebsocket.handler import WebSocketHandler
+
+    server = pywsgi.WSGIServer(("", 8000), websocket_app,
+        handler_class=WebSocketHandler)
+    server.serve_forever()
+
+The handler enhances your WSGI app with a Websocket environment variable when the
+browser requests a Websocket connection.
+
+::
+
+    def websocket_app(environ, start_response):
+        if environ["PATH_INFO"] == '/echo':
+            ws = environ["wsgi.websocket"]
+            message = ws.receive()
+            ws.send(message)
+
+Gunicorn Server
+^^^^^^^^^^^^^^^
+
+Using Gunicorn it is even more easy to start a server. Only the
+`websocket_app` from the previous example is required to start the server.
+Start Gunicorn using the following command and worker class to enable Websocket
+funtionality for the application.
+
+::
+
+    gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" wsgi:websocket_app
+
+Backwards incompatible changes
+------------------------------
+
+- The `wait()` method was renamed to `receive()`.
+
+.. _gevent-websocket: http://www.bitbucket.org/Jeffrey/gevent-websocket/
+.. _gevent: http://www.gevent.org/
+.. _Jeffrey Gelens: http://www.gelens.org/
+.. _PyPi: http://pypi.python.org/pypi/gevent-websocket/
+.. _repository: http://www.bitbucket.org/Jeffrey/gevent-websocket/

File examples/echoserver.py

+import os
+from gevent.pywsgi import WSGIServer
+import geventwebsocket
+
+
+def echo(environ, start_response):
+    websocket = environ.get("wsgi.websocket")
+    if websocket is None:
+        return http_handler(environ, start_response)
+    try:
+        while True:
+            message = websocket.receive()
+            if message is None:
+                break
+            websocket.send(message)
+        websocket.close()
+    except geventwebsocket.WebSocketError, ex:
+        print "%s: %s" % (ex.__class__.__name__, ex)
+
+
+def http_handler(environ, start_response):
+    if environ["PATH_INFO"].strip("/") == "version":
+        start_response("200 OK", [])
+        return [agent]
+    else:
+        start_response("400 Bad Request", [])
+        return ["WebSocket connection is expected here."]
+
+
+path = os.path.dirname(geventwebsocket.__file__)
+agent = "gevent-websocket/%s" % (geventwebsocket.__version__)
+print "Running %s from %s" % (agent, path)
+WSGIServer(("", 8000), echo, handler_class=geventwebsocket.WebSocketHandler).serve_forever()

File examples/plot_graph.html

+<!DOCTYPE html>
+<html>
+<head>
+	<!-- idea and code swiped from
+	http://assorted.svn.sourceforge.net/viewvc/assorted/real-time-plotter/trunk/src/rtp.html?view=markup -->
+	<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
+	<script src="http://people.iola.dk/olau/flot/jquery.flot.js"></script>
+	<script>
+	var iets = "";
+	window.onload = function() {
+		var data = {};
+		try {
+			var s = new WebSocket("ws://localhost:8000/data");
+		}
+		catch (e) {
+			var s = new MozWebSocket("ws://localhost:8000/data");
+		}
+		s.onopen = function() {
+			s.send('hi');
+		};
+		s.onmessage = function(e) {
+			var lines = e.data.split('\n');
+
+			for (var i = 0; i < lines.length - 1; i++) {
+				var parts = lines[i].split(' ');
+				var d = parts[0], x = parseFloat(parts[1]), y = parseFloat(parts[2]);
+				if (!(d in data)) data[d] = [];
+				data[d].push([x,y]);
+			}
+
+			var plots = [];
+			for (var d in data) plots.push({ data: data[d].slice(data[d].length - 200) });
+
+			$.plot($("#holder"), plots, {
+				series: { lines: { show: true, fill: true },},
+				yaxis: { min: 0 },
+			});
+			s.send('');
+		};
+	};
+	</script>
+</head>
+	<body>
+	<h3>Plot</h3>
+	<div id="holder" style="width:600px;height:300px"></div>
+	</body>
+</html>

File examples/plot_graph.py

+"""
+This example generates random data and plots a graph in the browser.
+
+Run it using Gevent directly using:
+    $ python plot_grapg.py
+
+Or with an Gunicorn wrapper:
+    $ gunicorn -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" \
+        plot_graph:app
+"""
+
+import gevent
+import os
+import random
+
+from gevent import pywsgi
+from geventwebsocket.handler import WebSocketHandler
+
+
+def handle(ws):
+    """
+    This is the websocket handler function. Note that we can dispatch based on
+    path in here, too.
+    """
+
+    if ws.path == "/echo":
+        while True:
+            m = ws.receive()
+            if m is None:
+                break
+            ws.send(m)
+
+    elif ws.path == "/data":
+        for i in xrange(10000):
+            ws.send("0 %s %s\n" % (i, random.random()))
+            gevent.sleep(0.1)
+
+
+def app(environ, start_response):
+    if environ["PATH_INFO"] == "/":
+        start_response("200 OK", [("Content-Type", "text/html")])
+        return open("plot_graph.html").readlines()
+    elif environ["PATH_INFO"] in ("/data", "/echo"):
+        handle(environ["wsgi.websocket"])
+    else:
+        start_response("404 Not Found", [])
+        return []
+
+
+if __name__ == "__main__":
+    server = pywsgi.WSGIServer(("", 8000), app,
+        handler_class=WebSocketHandler)
+    server.serve_forever()

File geventwebsocket/__init__.py

+version_info = (0, 3, 1)
+__version__ = ".".join(map(str, version_info))
+
+__all__ = ['WebSocketHandler', 'WebSocketError']
+
+from geventwebsocket.handler import WebSocketHandler
+from geventwebsocket.websocket import WebSocketError

File geventwebsocket/exceptions.py

+from socket import error as socket_error
+
+
+class WebSocketError(socket_error):
+    pass
+
+
+class FrameTooLargeException(WebSocketError):
+    pass

File geventwebsocket/gunicorn/workers.py

+from geventwebsocket.handler import WebSocketHandler
+from gunicorn.workers.ggevent import GeventPyWSGIWorker
+
+
+class GeventWebSocketWorker(GeventPyWSGIWorker):
+    wsgi_handler = WebSocketHandler

File geventwebsocket/handler.py

+import base64
+import re
+import struct
+from hashlib import md5, sha1
+from socket import error as socket_error
+from urllib import quote
+
+from gevent.pywsgi import WSGIHandler
+from geventwebsocket.websocket import WebSocketHybi, WebSocketHixie
+
+
+class WebSocketHandler(WSGIHandler):
+    """ Automatically upgrades the connection to websockets. """
+
+    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+    SUPPORTED_VERSIONS = ('13', '8', '7')
+
+    def handle_one_response(self):
+        self.pre_start()
+        environ = self.environ
+        upgrade = environ.get('HTTP_UPGRADE', '').lower()
+
+        if upgrade == 'websocket':
+            connection = environ.get('HTTP_CONNECTION', '').lower()
+            if 'upgrade' in connection:
+                return self._handle_websocket()
+        return super(WebSocketHandler, self).handle_one_response()
+
+    def pre_start(self):
+        pass
+
+    def _fake_start_response(self, *args, **kwargs):
+        return None
+
+    def _handle_websocket(self):
+        environ = self.environ
+
+        try:
+            if environ.get("HTTP_SEC_WEBSOCKET_VERSION"):
+                self.close_connection = True
+                result = self._handle_hybi()
+            elif environ.get("HTTP_ORIGIN"):
+                self.close_connection = True
+                result = self._handle_hixie()
+
+            self.result = []
+            if not result:
+                return
+
+            self.application(environ, self._fake_start_response)
+            return []
+        finally:
+            self.log_request()
+
+    def _handle_hybi(self):
+        environ = self.environ
+        version = environ.get("HTTP_SEC_WEBSOCKET_VERSION")
+
+        environ['wsgi.websocket_version'] = 'hybi-%s' % version
+
+        if version not in self.SUPPORTED_VERSIONS:
+            self.log_error('400: Unsupported Version: %r', version)
+            self.respond(
+                '400 Unsupported Version',
+                [('Sec-WebSocket-Version', '13, 8, 7')]
+            )
+            return
+
+        protocol, version = self.request_version.split("/")
+        key = environ.get("HTTP_SEC_WEBSOCKET_KEY")
+
+        # check client handshake for validity
+        if not environ.get("REQUEST_METHOD") == "GET":
+            # 5.2.1 (1)
+            self.respond('400 Bad Request')
+            return
+        elif not protocol == "HTTP":
+            # 5.2.1 (1)
+            self.respond('400 Bad Request')
+            return
+        elif float(version) < 1.1:
+            # 5.2.1 (1)
+            self.respond('400 Bad Request')
+            return
+        # XXX: nobody seems to set SERVER_NAME correctly. check the spec
+        #elif not environ.get("HTTP_HOST") == environ.get("SERVER_NAME"):
+            # 5.2.1 (2)
+            #self.respond('400 Bad Request')
+            #return
+        elif not key:
+            # 5.2.1 (3)
+            self.log_error('400: HTTP_SEC_WEBSOCKET_KEY is missing from request')
+            self.respond('400 Bad Request')
+            return
+        elif len(base64.b64decode(key)) != 16:
+            # 5.2.1 (3)
+            self.log_error('400: Invalid key: %r', key)
+            self.respond('400 Bad Request')
+            return
+
+        self.websocket = WebSocketHybi(self.socket, environ)
+        environ['wsgi.websocket'] = self.websocket
+
+        headers = [
+            ("Upgrade", "websocket"),
+            ("Connection", "Upgrade"),
+            ("Sec-WebSocket-Accept", base64.b64encode(sha1(key + self.GUID).digest())),
+        ]
+        self._send_reply("101 Switching Protocols", headers)
+        return True
+
+    def _handle_hixie(self):
+        environ = self.environ
+        assert "upgrade" in self.environ.get("HTTP_CONNECTION", "").lower()
+
+        self.websocket = WebSocketHixie(self.socket, environ)
+        environ['wsgi.websocket'] = self.websocket
+
+        key1 = self.environ.get('HTTP_SEC_WEBSOCKET_KEY1')
+        key2 = self.environ.get('HTTP_SEC_WEBSOCKET_KEY2')
+
+        if key1 is not None:
+            environ['wsgi.websocket_version'] = 'hixie-76'
+            if not key1:
+                self.log_error("400: SEC-WEBSOCKET-KEY1 header is empty")
+                self.respond('400 Bad Request')
+                return
+            if not key2:
+                self.log_error("400: SEC-WEBSOCKET-KEY2 header is missing or empty")
+                self.respond('400 Bad Request')
+                return
+
+            part1 = self._get_key_value(key1)
+            part2 = self._get_key_value(key2)
+            if part1 is None or part2 is None:
+                self.respond('400 Bad Request')
+                return
+
+            headers = [
+                ("Upgrade", "WebSocket"),
+                ("Connection", "Upgrade"),
+                ("Sec-WebSocket-Location", reconstruct_url(environ)),
+            ]
+            if self.websocket.protocol is not None:
+                headers.append(("Sec-WebSocket-Protocol", self.websocket.protocol))
+            if self.websocket.origin:
+                headers.append(("Sec-WebSocket-Origin", self.websocket.origin))
+
+            self._send_reply("101 Web Socket Protocol Handshake", headers)
+
+            # This request should have 8 bytes of data in the body
+            key3 = self.rfile.read(8)
+
+            challenge = md5(struct.pack("!II", part1, part2) + key3).digest()
+
+            self.socket.sendall(challenge)
+            return True
+        else:
+            environ['wsgi.websocket_version'] = 'hixie-75'
+            headers = [
+                ("Upgrade", "WebSocket"),
+                ("Connection", "Upgrade"),
+                ("WebSocket-Location", reconstruct_url(environ)),
+            ]
+
+            if self.websocket.protocol is not None:
+                headers.append(("WebSocket-Protocol", self.websocket.protocol))
+            if self.websocket.origin:
+                headers.append(("WebSocket-Origin", self.websocket.origin))
+
+            self._send_reply("101 Web Socket Protocol Handshake", headers)
+
+    def _send_reply(self, status, headers):
+        self.status = status
+
+        towrite = []
+        towrite.append('%s %s\r\n' % (self.request_version, self.status))
+
+        for header in headers:
+            towrite.append("%s: %s\r\n" % header)
+
+        towrite.append("\r\n")
+        msg = ''.join(towrite)
+        self.socket.sendall(msg)
+        self.headers_sent = True
+
+    def respond(self, status, headers=[]):
+        self.close_connection = True
+        self._send_reply(status, headers)
+
+        if self.socket is not None:
+            try:
+                self.socket._sock.close()
+                self.socket.close()
+            except socket_error:
+                pass
+
+    def _get_key_value(self, key_value):
+        key_number = int(re.sub("\\D", "", key_value))
+        spaces = re.subn(" ", "", key_value)[1]
+
+        if key_number % spaces != 0:
+            self.log_error("key_number %d is not an intergral multiple of spaces %d", key_number, spaces)
+        else:
+            return key_number / spaces
+
+
+def reconstruct_url(environ):
+    secure = environ['wsgi.url_scheme'] == 'https'
+    if secure:
+        url = 'wss://'
+    else:
+        url = 'ws://'
+
+    if environ.get('HTTP_HOST'):
+        url += environ['HTTP_HOST']
+    else:
+        url += environ['SERVER_NAME']
+
+        if secure:
+            if environ['SERVER_PORT'] != '443':
+                url += ':' + environ['SERVER_PORT']
+        else:
+            if environ['SERVER_PORT'] != '80':
+                url += ':' + environ['SERVER_PORT']
+
+    url += quote(environ.get('SCRIPT_NAME', ''))
+    url += quote(environ.get('PATH_INFO', ''))
+
+    if environ.get('QUERY_STRING'):
+        url += '?' + environ['QUERY_STRING']
+
+    return url

File geventwebsocket/python_fixes.py

+import sys
+
+
+if sys.version_info[:2] == (2, 7):
+    # Python 2.7 has a working BufferedReader but socket.makefile() does not
+    # use it.
+    # Python 2.6's BufferedReader is broken (TypeError: recv_into() argument
+    # 1 must be pinned buffer, not bytearray).
+    from io import BufferedReader, RawIOBase
+
+    class SocketIO(RawIOBase):
+        def __init__(self, sock):
+            RawIOBase.__init__(self)
+            self._sock = sock
+
+        def readinto(self, b):
+            self._checkClosed()
+            while True:
+                try:
+                    return self._sock.recv_into(b)
+                except socket_error as ex:
+                    if ex.args[0] == EINTR:
+                        continue
+                    raise
+
+        def readable(self):
+            return self._sock is not None
+
+        @property
+        def closed(self):
+            return self._sock is None
+
+        def fileno(self):
+            self._checkClosed()
+            return self._sock.fileno()
+
+        @property
+        def name(self):
+            if not self.closed:
+                return self.fileno()
+            else:
+                return -1
+
+        def close(self):
+            if self._sock is None:
+                return
+            else:
+                self._sock.close()
+                self._sock = None
+                RawIOBase.close(self)
+
+    def makefile(socket):
+        return BufferedReader(SocketIO(socket))
+
+else:
+    def makefile(socket):
+        # XXX on python3 enable buffering
+        return socket.makefile()
+
+
+if sys.version_info[:2] < (2, 7):
+    def is_closed(fobj):
+        return fobj._sock is None
+else:
+    def is_closed(fobj):
+        return fobj.closed

File geventwebsocket/websocket.py

+import struct
+
+from errno import EINTR
+from gevent.coros import Semaphore
+
+from python_fixes import makefile, is_closed
+from exceptions import FrameTooLargeException, WebSocketError
+
+
+class WebSocket(object):
+    def _encode_text(self, text):
+        if isinstance(text, unicode):
+            return text.encode('utf-8')
+        else:
+            return text
+
+
+class WebSocketHixie(WebSocket):
+    def __init__(self, socket, environ):
+        self.origin = environ.get('HTTP_ORIGIN')
+        self.protocol = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL')
+        self.path = environ.get('PATH_INFO')
+        self.fobj = socket.makefile()
+        self._writelock = Semaphore(1)
+        self._write = socket.sendall
+
+    def send(self, message):
+        message = self._encode_text(message)
+
+        with self._writelock:
+            self._write("\x00" + message + "\xFF")
+
+    def close(self):
+        if self.fobj is not None:
+            self.fobj.close()
+            self.fobj = None
+            self._write = None
+
+    def _message_length(self):
+        length = 0
+
+        while True:
+            if self.fobj is None:
+                raise WebSocketError('Connection closed unexpectedly while reading message length')
+            byte_str = self.fobj.read(1)
+
+            if not byte_str:
+                return 0
+            else:
+                byte = ord(byte_str)
+
+            if byte != 0x00:
+                length = length * 128 + (byte & 0x7f)
+                if (byte & 0x80) != 0x80:
+                    break
+
+        return length
+
+    def _read_until(self):
+        bytes = []
+
+        read = self.fobj.read
+
+        while True:
+            if self.fobj is None:
+                msg = ''.join(bytes)
+                raise WebSocketError('Connection closed unexpectedly while reading message: %r' % msg)
+
+            byte = read(1)
+            if ord(byte) != 0xff:
+                bytes.append(byte)
+            else:
+                break
+
+        return ''.join(bytes)
+
+    def receive(self):
+        read = self.fobj.read
+
+        while self.fobj is not None:
+            frame_str = read(1)
+
+            if not frame_str:
+                self.close()
+                return
+            else:
+                frame_type = ord(frame_str)
+
+            if frame_type == 0x00:
+                bytes = self._read_until()
+                return bytes.decode("utf-8", "replace")
+            else:
+                raise WebSocketError("Received an invalid frame_type=%r" % frame_type)
+
+
+class WebSocketHybi(WebSocket):
+    OPCODE_TEXT = 0x1
+    OPCODE_BINARY = 0x2
+    OPCODE_CLOSE = 0x8
+    OPCODE_PING = 0x9
+    OPCODE_PONG = 0xA
+
+    def __init__(self, socket, environ):
+        self.origin = environ.get('HTTP_SEC_WEBSOCKET_ORIGIN')
+        self.protocol = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL', 'unknown')
+        self.path = environ.get('PATH_INFO')
+        self._chunks = bytearray()
+        self._writelock = Semaphore(1)
+        self.socket = socket
+        self._write = socket.sendall
+        self.fobj = makefile(socket)
+        self.close_code = None
+        self.close_message = None
+        self._reading = False
+
+    def _parse_header(self, data):
+        if len(data) != 2:
+            self._close()
+            raise WebSocketError('Incomplete read while reading header: %r' % data)
+
+        first_byte, second_byte = struct.unpack('!BB', data)
+
+        fin = (first_byte >> 7) & 1
+        rsv1 = (first_byte >> 6) & 1
+        rsv2 = (first_byte >> 5) & 1
+        rsv3 = (first_byte >> 4) & 1
+        opcode = first_byte & 0xf
+
+        # frame-fin = %x0 ; more frames of this message follow
+        #           / %x1 ; final frame of this message
+
+        # frame-rsv1 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise
+        # frame-rsv2 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise
+        # frame-rsv3 = %x0 ; 1 bit, MUST be 0 unless negotiated otherwise
+        if rsv1 or rsv2 or rsv3:
+            self.close(1002)
+            raise WebSocketError('Received frame with non-zero reserved bits: %r' % str(data))
+
+        if opcode > 0x7 and fin == 0:
+            self.close(1002)
+            raise WebSocketError('Received fragmented control frame: %r' % str(data))
+
+        if len(self._chunks) > 0 and fin == 0 and not opcode:
+            self.close(1002)
+            raise WebSocketError('Received new fragment frame with non-zero opcode: %r' % str(data))
+
+        if len(self._chunks) > 0 and fin == 1 and (self.OPCODE_TEXT <= opcode <= self.OPCODE_BINARY):
+            self.close(1002)
+            raise WebSocketError('Received new unfragmented data frame during fragmented message: %r' % str(data))
+
+        has_mask = (second_byte >> 7) & 1
+        length = (second_byte) & 0x7f
+
+        # Control frames MUST have a payload length of 125 bytes or less
+        if opcode > 0x7 and length > 125:
+            self.close(1002)
+            raise FrameTooLargeException("Control frame payload cannot be larger than 125 bytes: %r" % str(data))
+
+        return fin, opcode, has_mask, length
+
+    def receive_frame(self):
+        """Return the next frame from the socket."""
+        fobj = self.fobj
+
+        if fobj is None:
+            return
+
+        if is_closed(fobj):
+            return
+
+        read = self.fobj.read
+
+        assert not self._reading, 'Reading is not possible from multiple greenlets'
+        self._reading = True
+
+        try:
+            data0 = read(2)
+
+            if not data0:
+                self._close()
+                return
+
+            fin, opcode, has_mask, length = self._parse_header(data0)
+
+            if not has_mask and length:
+                self.close(1002)
+                raise WebSocketError('Message from client is not masked')
+
+            if length < 126:
+                data1 = ''
+            elif length == 126:
+                data1 = read(2)
+
+                if len(data1) != 2:
+                    self.close()
+                    raise WebSocketError('Incomplete read while reading 2-byte length: %r' % (data0 + data1))
+
+                length = struct.unpack('!H', data1)[0]
+            else:
+                assert length == 127, length
+                data1 = read(8)
+
+                if len(data1) != 8:
+                    self.close()
+                    raise WebSocketError('Incomplete read while reading 8-byte length: %r' % (data0 + data1))
+
+                length = struct.unpack('!Q', data1)[0]
+
+            mask = read(4)
+            if len(mask) != 4:
+                self._close()
+                raise WebSocketError('Incomplete read while reading mask: %r' % (data0 + data1 + mask))
+
+            mask = struct.unpack('!BBBB', mask)
+
+            if length:
+                payload = read(length)
+                if len(payload) != length:
+                    self._close()
+                    args = (length, len(payload))
+                    raise WebSocketError('Incomplete read: expected message of %s bytes, got %s bytes' % args)
+            else:
+                payload = ''
+
+            if payload:
+                payload = bytearray(payload)
+
+                for i in xrange(len(payload)):
+                    payload[i] = payload[i] ^ mask[i % 4]
+
+            return fin, opcode, payload
+        finally:
+            self._reading = False
+            if self.fobj is None:
+                fobj.close()
+
+    def _receive(self):
+        """Return the next text or binary message from the socket."""
+
+        opcode = None
+        result = bytearray()
+
+        while True:
+            frame = self.receive_frame()
+            if frame is None:
+                if result:
+                    raise WebSocketError('Peer closed connection unexpectedly')
+                return
+
+            f_fin, f_opcode, f_payload = frame
+
+            if f_opcode in (self.OPCODE_TEXT, self.OPCODE_BINARY):
+                if opcode is None:
+                    opcode = f_opcode
+                else:
+                    raise WebSocketError('The opcode in non-fin frame is expected to be zero, got %r' % (f_opcode, ))
+            elif not f_opcode:
+                if opcode is None:
+                    self.close(1002)
+                    raise WebSocketError('Unexpected frame with opcode=0')
+            elif f_opcode == self.OPCODE_CLOSE:
+                if len(f_payload) >= 2:
+                    self.close_code = struct.unpack('!H', str(f_payload[:2]))[0]
+                    self.close_message = f_payload[2:]
+                elif f_payload:
+                    self._close()
+                    raise WebSocketError('Invalid close frame: %s %s %s' % (f_fin, f_opcode, repr(f_payload)))
+                code = self.close_code
+                if code is None or (code >= 1000 and code < 5000):
+                    self.close()
+                else:
+                    self.close(1002)
+                    raise WebSocketError('Received invalid close frame: %r %r' % (code, self.close_message))
+                return
+            elif f_opcode == self.OPCODE_PING:
+                self.send_frame(f_payload, opcode=self.OPCODE_PONG)
+                continue
+            elif f_opcode == self.OPCODE_PONG:
+                continue
+            else:
+                self._close()  # XXX should send proper reason?
+                raise WebSocketError("Unexpected opcode=%r" % (f_opcode, ))
+
+            result.extend(f_payload)
+            if f_fin:
+                break
+
+        if opcode == self.OPCODE_TEXT:
+            return result, False
+        elif opcode == self.OPCODE_BINARY:
+            return result, True
+        else:
+            raise AssertionError('internal serror in gevent-websocket: opcode=%r' % (opcode, ))
+
+    def receive(self):
+        result = self._receive()
+        if not result:
+            return result
+
+        message, is_binary = result
+        if is_binary:
+            return message
+        else:
+            try:
+                return message.decode('utf-8')
+            except ValueError:
+                self.close(1007)
+                raise
+
+    def send_frame(self, message, opcode):
+        """Send a frame over the websocket with message as its payload"""
+
+        if self.socket is None:
+            raise WebSocketError('The connection was closed')
+
+        header = chr(0x80 | opcode)
+
+        if isinstance(message, unicode):
+            message = message.encode('utf-8')
+
+        msg_length = len(message)
+
+        if msg_length < 126:
+            header += chr(msg_length)
+        elif msg_length < (1 << 16):
+            header += chr(126) + struct.pack('!H', msg_length)
+        elif msg_length < (1 << 63):
+            header += chr(127) + struct.pack('!Q', msg_length)
+        else:
+            raise FrameTooLargeException()
+
+        try:
+            combined = header + message
+        except TypeError:
+            with self._writelock:
+                self._write(header)
+                self._write(message)
+        else:
+            with self._writelock:
+                self._write(combined)
+
+    def send(self, message, binary=None):
+        """Send a frame over the websocket with message as its payload"""
+        if binary is None:
+            binary = not isinstance(message, (str, unicode))
+
+        if binary:
+            return self.send_frame(message, self.OPCODE_BINARY)
+        else:
+            return self.send_frame(message, self.OPCODE_TEXT)
+
+    def close(self, code=1000, message=''):
+        """Close the websocket, sending the specified code and message"""
+        if self.socket is not None:
+            message = self._encode_text(message)
+            self.send_frame(struct.pack('!H%ds' % len(message), code, message), opcode=self.OPCODE_CLOSE)
+            self._close()
+
+    def _close(self):
+        if self.socket is not None:
+            self.socket = None
+            self._write = None
+
+            if not self._reading:
+                self.fobj.close()
+
+            self.fobj = None
+from setuptools import setup, find_packages
+
+setup(
+    name="gevent-websocket",
+    version="0.3.1",
+    description="Websocket handler for the gevent pywsgi server, a Python network library",
+    long_description=open("README.rst").read(),
+    author="Jeffrey Gelens",
+    author_email="jeffrey@noppo.pro",
+    license="BSD",
+    url="https://bitbucket.org/Jeffrey/gevent-websocket",
+    download_url="https://bitbucket.org/Jeffrey/gevent-websocket",
+    install_requires=("gevent", "greenlet"),
+    packages=find_packages(exclude=["examples","tests"]),
+    classifiers=[
+        "Development Status :: 4 - Beta",
+        "License :: OSI Approved :: BSD License",
+        "Programming Language :: Python",
+        "Operating System :: MacOS :: MacOS X",
+        "Operating System :: POSIX",
+        "Topic :: Internet",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        "Intended Audience :: Developers",
+    ],
+)

File tests/run_autobahn_tests.py

+#!/usr/bin/env python
+"""Test gevent-websocket with the test suite of Autobahn
+
+    http://www.tavendo.de/autobahn/testsuite.html
+"""
+import sys
+import os
+import subprocess
+import time
+import urllib2
+from twisted.python import log
+from twisted.internet import reactor
+from autobahn.fuzzing import FuzzingClientFactory
+
+
+spec = {
+   "options": {"failByDrop": False},
+   "enable-ssl": False,
+   "servers": []}
+
+
+default_args = ["*",
+         "x7.5.1",
+         "x7.9.3",
+         "x7.9.4",
+         "x7.9.5",
+         "x7.9.6",
+         "x7.9.7",
+         "x7.9.8",
+         "x7.9.9",
+         "x7.9.10",
+         "x7.9.11",
+         "x7.9.12",
+         "x7.9.13"]
+# We ignore 7.5.1 because it checks that close frame has valid utf-8 message
+# we do not validate utf-8.
+
+# We ignore 7.9.3-13 because it checks that when a close frame with code 1004
+# and others sent, 1002 is sent back; we only send back 1002 for obvious
+# violations like < 1000 and >= 5000; for all codes in the 1000-5000 range
+# we send code 1000 back
+
+
+class ProcessPool(object):
+
+    def __init__(self):
+        self.popens = []
+
+    def spawn(self, *args, **kwargs):
+        popen = subprocess.Popen(*args, **kwargs)
+        self.popens.append(popen)
+        time.sleep(0.2)
+        self.check()
+        return popen
+
+    def check(self):
+        for popen in self.popens:
+            if popen.poll() is not None:
+                sys.exit(1)
+
+    def wait(self, timeout):
+        end = time.time() + timeout
+        while True:
+            time.sleep(0.1)
+            pool.check()
+            if time.time() > end:
+                break
+
+    def kill(self):
+        while self.popens:
+            popen = self.popens.pop()
+            try:
+                popen.kill()
+            except Exception, ex:
+                print ex
+
+
+if __name__ == '__main__':
+    import optparse
+    parser = optparse.OptionParser()
+    parser.add_option('--geventwebsocket', default='../examples/echoserver.py')
+    parser.add_option('--autobahn', default='../../src/Autobahn/testsuite/websockets/servers/test_autobahn.py')
+    options, args = parser.parse_args()
+
+    cases = []
+    exclude_cases = []
+
+    for arg in (args or default_args):
+        if arg.startswith('x'):
+            arg = arg[1:]
+            exclude_cases.append(arg)
+        else:
+            cases.append(arg)
+
+    spec['cases'] = cases
+    spec['exclude-cases'] = exclude_cases
+
+    if options.autobahn and not os.path.exists(options.autobahn):
+        print 'Ignoring %s (not found)' % options.autobahn
+        options.autobahn = None
+    pool = ProcessPool()
+    try:
+        if options.geventwebsocket:
+            pool.spawn([sys.executable, options.geventwebsocket])
+        if options.autobahn:
+            pool.spawn([sys.executable, options.autobahn])
+        pool.wait(1)
+        if options.geventwebsocket:
+            agent = urllib2.urlopen('http://127.0.0.1:8000/version').read().strip()
+            assert agent and '\n' not in agent and 'gevent-websocket' in agent, agent
+            spec['servers'].append({"url": "ws://localhost:8000",
+                                    "agent": agent,
+                                    "options": {"version": 17}})
+        if options.autobahn:
+            spec['servers'].append({'url': 'ws://localhost:8000/',
+                                    'agent': 'AutobahnServer',
+                                    'options': {'version': 17}})
+        log.startLogging(sys.stdout)
+        FuzzingClientFactory(spec)
+        reactor.run()
+    finally:
+        pool.kill()