Pārlūkot izejas kodu

Update MT server to use supported websocket, which has Python3 capabilities.

Bentley James Oakes 7 gadi atpakaļ
vecāks
revīzija
9dda680b7a

+ 0 - 240
mt/___websocket.py

@@ -1,240 +0,0 @@
-"""
-REF:: https://github.com/mtah/python-websocket (+ /.setup/websocket.py.patch)
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program.  If not, see <http://www.gnu.org/licenses/>
-"""
-
-import sys, re, socket, asyncore
-
-if sys.version_info[0] < 3:
-    import urlparse as urlparse
-else:
-    import urllib.parse as urlparse
-
-class WebSocket(object):
-    def __init__(self, url, **kwargs):
-        self.host, self.port, self.resource, self.secure = WebSocket._parse_url(url)
-        self.protocol = kwargs.pop('protocol', None)
-        self.cookie_jar = kwargs.pop('cookie_jar', None)
-        self.onopen = kwargs.pop('onopen', None)
-        self.onmessage = kwargs.pop('onmessage', None)
-        self.onerror = kwargs.pop('onerror', None)
-        self.onclose = kwargs.pop('onclose', None)
-        if kwargs: raise ValueError('Unexpected argument(s): %s' % ', '.join(list(kwargs.values())))
-
-        self._dispatcher = _Dispatcher(self)
-
-    def send(self, data):
-        self._dispatcher.write('\x00' + _utf8(data) + '\xff')
-
-    def close(self):
-        self._dispatcher.handle_close()
-
-    @classmethod
-    def _parse_url(cls, url):
-        p = urlparse.urlparse(url)
-
-        if p.hostname:
-            host = p.hostname
-        else:
-            raise ValueError('URL must be absolute')
-
-        if p.fragment:
-            raise ValueError('URL must not contain a fragment component')
-
-        if p.scheme == 'ws':
-            secure = False
-            port = p.port or 80
-        elif p.scheme == 'wss':
-            raise NotImplementedError('Secure WebSocket not yet supported')
-            # secure = True
-            # port = p.port or 443
-        else:
-            raise ValueError('Invalid URL scheme')
-
-        resource = p.path or '/'
-        if p.query: resource += '?' + p.query
-        return (host, port, resource, secure)
-
-    #@classmethod
-    #def _generate_key(cls):
-    #    spaces = random.randint(1, 12)
-    #    number = random.randint(0, 0xffffffff/spaces)
-    #    key = list(str(number*spaces))
-    #    chars = map(unichr, range(0x21, 0x2f) + range(0x3a, 0x7e))
-    #    random_inserts = random.sample(xrange(len(key)), random.randint(1,12))
-    #    for (i, c) in [(r, random.choice(chars)) for r in random_inserts]:
-    #        key.insert(i, c)
-    #    print key
-    #    return ''.join(key)
-
-class WebSocketError(Exception):
-    def _init_(self, value):
-        self.value = value
-
-    def _str_(self):
-        return str(self.value)
-
-class _Dispatcher(asyncore.dispatcher):
-    def __init__(self, ws):
-        asyncore.dispatcher.__init__(self)
-        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
-        self.connect((ws.host, ws.port))
-
-        self.ws = ws
-        self._read_buffer = ''
-        self._write_buffer = ''
-        self._handshake_complete = False
-
-        if self.ws.port != 80:
-            hostport = '%s:%d' % (self.ws.host, self.ws.port)
-        else:
-            hostport = self.ws.host
-
-        fields = [
-            'Upgrade: WebSocket',
-            'Connection: Upgrade',
-            'Host: ' + hostport,
-            'Origin: http://' + hostport,
-            #'Sec-WebSocket-Key1: %s' % WebSocket.generate_key(),
-            #'Sec-WebSocket-Key2: %s' % WebSocket.generate_key()
-        ]
-        if self.ws.protocol: fields['Sec-WebSocket-Protocol'] = self.ws.protocol
-        if self.ws.cookie_jar:
-            cookies = [c for c in self.ws.cookie_jar if _cookie_for_domain(c, _eff_host(self.ws.host)) and \
-                       _cookie_for_path(c, self.ws.resource) and \
-                       not c.is_expired()]
-
-            for cookie in cookies:
-                fields.append('Cookie: %s=%s' % (cookie.name, cookie.value))
-
-        # key3 = ''.join(map(unichr, (random.randrange(256) for i in xrange(8))))
-        self.write(_utf8('GET %s HTTP/1.1\r\n' \
-                         '%s\r\n\r\n' % (self.ws.resource,
-                                         '\r\n'.join(fields))))
-        # key3)))
-
-    def handl_expt(self):
-        self.handle_error()
-
-    def handle_error(self):
-        self.close()
-        t, e, trace = sys.exc_info()
-        if self.ws.onerror:
-            self.ws.onerror(e)
-        else:
-            asyncore.dispatcher.handle_error(self)
-
-    def handle_close(self):
-        self.close()
-        if self.ws.onclose:
-            self.ws.onclose()
-
-    def handle_read(self):
-        if self._handshake_complete:
-            self._read_until('\xff', self._handle_frame)
-        else:
-            self._read_until('\r\n\r\n', self._handle_header)
-
-    def handle_write(self):
-        sent = self.send(self._write_buffer)
-        self._write_buffer = self._write_buffer[sent:]
-
-    def writable(self):
-        return len(self._write_buffer) > 0
-
-    def write(self, data):
-        self._write_buffer += data # TODO: separate buffer for handshake from data to
-        # prevent mix-up when send() is called before
-        # handshake is complete?
-
-    def _read_until(self, delimiter, callback):
-        def lookForAndHandleCompletedFrame():
-            pos = self._read_buffer.find(delimiter)
-            if pos >= 0:
-                pos += len(delimiter)
-                data = self._read_buffer[:pos]
-                self._read_buffer = self._read_buffer[pos:]
-                if data:
-                    callback(data)
-                    lookForAndHandleCompletedFrame()
-
-        self._read_buffer += self.recv(4096)
-        lookForAndHandleCompletedFrame()
-
-    def _handle_frame(self, frame):
-        assert frame[-1] == '\xff'
-        if frame[0] != '\x00':
-            raise WebSocketError('WebSocket stream error')
-
-        if self.ws.onmessage:
-            self.ws.onmessage(frame[1:-1])
-        # TODO: else raise WebSocketError('No message handler defined')
-
-    def _handle_header(self, header):
-        assert header[-4:] == '\r\n\r\n'
-        start_line, fields = _parse_http_header(header)
-        if start_line != 'HTTP/1.1 101 Web Socket Protocol Handshake' or \
-                fields.get('Connection', None) != 'Upgrade' or \
-                fields.get('Upgrade', None) != 'WebSocket':
-            raise WebSocketError('Invalid server handshake')
-
-        self._handshake_complete = True
-        if self.ws.onopen:
-            self.ws.onopen()
-
-_IPV4_RE = re.compile(r'\.\d+$')
-_PATH_SEP = re.compile(r'/+')
-
-def _parse_http_header(header):
-    def split_field(field):
-        k, v = field.split(':', 1)
-        return (k, v.strip())
-
-    lines = header.strip().split('\r\n')
-    if len(lines) > 0:
-        start_line = lines[0]
-    else:
-        start_line = None
-
-    return (start_line, dict(map(split_field, lines[1:])))
-
-def _eff_host(host):
-    if host.find('.') == -1 and not _IPV4_RE.search(host):
-        return host + '.local'
-    return host
-
-def _cookie_for_path(cookie, path):
-    if not cookie.path or path == '' or path == '/':
-        return True
-    path = _PATH_SEP.split(path)[1:]
-    cookie_path = _PATH_SEP.split(cookie.path)[1:]
-    for p1, p2 in map(lambda *ps: ps, path, cookie_path):
-        if p1 == None:
-            return True
-        elif p1 != p2:
-            return False
-
-    return True
-
-def _cookie_for_domain(cookie, domain):
-    if not cookie.domain:
-        return True
-    elif cookie.domain[0] == '.':
-        return domain.endswith(cookie.domain)
-    else:
-        return cookie.domain == domain
-
-def _utf8(s):
-    return s.encode('utf-8')

+ 3 - 26
mt/main.py

@@ -2,41 +2,18 @@
 Copyright 2011 by the AToMPM team and licensed under the LGPL
 See COPYING.lesser and README.md in the root of this project for full details'''
 
-import asyncore, logging, threading
+import logging
 from httpd import HTTPServerThread
-from ws import WebSocket
 
 
 '''
-	init and launch http server and asyncore loop + set logging level for mt/*
-	
-	NOTE: omitting asyncore.loop()'s first parameter ('timeout' according to the
-  			API) causes initialization to be very long... it seems it represents
-			the delay during which an asyncore loop may remain idle before giving
-			control to someone
-
-	NOTE: python-websocket is built on an event-loop called asyncore... this loop
-  		  	is started via asyncore.loop()... unfortunately, if there isn't already
-			something in the loop when it's started, it just terminates... hence, 
-			to enable the creation of WebSockets (which requires a running asyncore
-			loop) in each future mtworker, we create a dummy WebSocket which serves
-			only to keep the asyncore loop alive while there are no other open
-			WebSockets '''
+	init and launch http server + set logging level for mt/*
+'''
 def main() :
 	logging.basicConfig(format='%(levelname)s - %(message)s', level=logging.INFO)
-	dummy_ws = WebSocket()
 	httpd = HTTPServerThread()
 	httpd.start()
 
-	try :
-		asyncore.loop(1)
-	except KeyboardInterrupt :
-		#		print(threading.enumerate())
-		httpd.stop()
-		dummy_ws.close()
-		pass
-
-
 if __name__ == "__main__" :
 	main()
 

+ 3 - 0
mt/websocket/README

@@ -0,0 +1,3 @@
+websocket-client
+https://github.com/websocket-client/websocket-client/
+version 0.48.0 - May 27th, 2018

+ 29 - 0
mt/websocket/__init__.py

@@ -0,0 +1,29 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+from ._abnf import *
+from ._app import WebSocketApp
+from ._core import *
+from ._exceptions import *
+from ._logging import *
+from ._socket import *
+
+__version__ = "0.48.0"

+ 447 - 0
mt/websocket/_abnf.py

@@ -0,0 +1,447 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+import array
+import os
+import struct
+
+import six
+
+from ._exceptions import *
+from ._utils import validate_utf8
+from threading import Lock
+
+try:
+    if six.PY3:
+        import numpy
+    else:
+        numpy = None
+except ImportError:
+    numpy = None
+
+try:
+    # If wsaccel is available we use compiled routines to mask data.
+    if not numpy:
+        from wsaccel.xormask import XorMaskerSimple
+
+        def _mask(_m, _d):
+            return XorMaskerSimple(_m).process(_d)
+except ImportError:
+    # wsaccel is not available, we rely on python implementations.
+    def _mask(_m, _d):
+        for i in range(len(_d)):
+            _d[i] ^= _m[i % 4]
+
+        if six.PY3:
+            return _d.tobytes()
+        else:
+            return _d.tostring()
+
+
+__all__ = [
+    'ABNF', 'continuous_frame', 'frame_buffer',
+    'STATUS_NORMAL',
+    'STATUS_GOING_AWAY',
+    'STATUS_PROTOCOL_ERROR',
+    'STATUS_UNSUPPORTED_DATA_TYPE',
+    'STATUS_STATUS_NOT_AVAILABLE',
+    'STATUS_ABNORMAL_CLOSED',
+    'STATUS_INVALID_PAYLOAD',
+    'STATUS_POLICY_VIOLATION',
+    'STATUS_MESSAGE_TOO_BIG',
+    'STATUS_INVALID_EXTENSION',
+    'STATUS_UNEXPECTED_CONDITION',
+    'STATUS_BAD_GATEWAY',
+    'STATUS_TLS_HANDSHAKE_ERROR',
+]
+
+# closing frame status codes.
+STATUS_NORMAL = 1000
+STATUS_GOING_AWAY = 1001
+STATUS_PROTOCOL_ERROR = 1002
+STATUS_UNSUPPORTED_DATA_TYPE = 1003
+STATUS_STATUS_NOT_AVAILABLE = 1005
+STATUS_ABNORMAL_CLOSED = 1006
+STATUS_INVALID_PAYLOAD = 1007
+STATUS_POLICY_VIOLATION = 1008
+STATUS_MESSAGE_TOO_BIG = 1009
+STATUS_INVALID_EXTENSION = 1010
+STATUS_UNEXPECTED_CONDITION = 1011
+STATUS_BAD_GATEWAY = 1014
+STATUS_TLS_HANDSHAKE_ERROR = 1015
+
+VALID_CLOSE_STATUS = (
+    STATUS_NORMAL,
+    STATUS_GOING_AWAY,
+    STATUS_PROTOCOL_ERROR,
+    STATUS_UNSUPPORTED_DATA_TYPE,
+    STATUS_INVALID_PAYLOAD,
+    STATUS_POLICY_VIOLATION,
+    STATUS_MESSAGE_TOO_BIG,
+    STATUS_INVALID_EXTENSION,
+    STATUS_UNEXPECTED_CONDITION,
+    STATUS_BAD_GATEWAY,
+)
+
+
+class ABNF(object):
+    """
+    ABNF frame class.
+    see http://tools.ietf.org/html/rfc5234
+    and http://tools.ietf.org/html/rfc6455#section-5.2
+    """
+
+    # operation code values.
+    OPCODE_CONT = 0x0
+    OPCODE_TEXT = 0x1
+    OPCODE_BINARY = 0x2
+    OPCODE_CLOSE = 0x8
+    OPCODE_PING = 0x9
+    OPCODE_PONG = 0xa
+
+    # available operation code value tuple
+    OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
+               OPCODE_PING, OPCODE_PONG)
+
+    # opcode human readable string
+    OPCODE_MAP = {
+        OPCODE_CONT: "cont",
+        OPCODE_TEXT: "text",
+        OPCODE_BINARY: "binary",
+        OPCODE_CLOSE: "close",
+        OPCODE_PING: "ping",
+        OPCODE_PONG: "pong"
+    }
+
+    # data length threshold.
+    LENGTH_7 = 0x7e
+    LENGTH_16 = 1 << 16
+    LENGTH_63 = 1 << 63
+
+    def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
+                 opcode=OPCODE_TEXT, mask=1, data=""):
+        """
+        Constructor for ABNF.
+        please check RFC for arguments.
+        """
+        self.fin = fin
+        self.rsv1 = rsv1
+        self.rsv2 = rsv2
+        self.rsv3 = rsv3
+        self.opcode = opcode
+        self.mask = mask
+        if data is None:
+            data = ""
+        self.data = data
+        self.get_mask_key = os.urandom
+
+    def validate(self, skip_utf8_validation=False):
+        """
+        validate the ABNF frame.
+        skip_utf8_validation: skip utf8 validation.
+        """
+        if self.rsv1 or self.rsv2 or self.rsv3:
+            raise WebSocketProtocolException("rsv is not implemented, yet")
+
+        if self.opcode not in ABNF.OPCODES:
+            raise WebSocketProtocolException("Invalid opcode %r", self.opcode)
+
+        if self.opcode == ABNF.OPCODE_PING and not self.fin:
+            raise WebSocketProtocolException("Invalid ping frame.")
+
+        if self.opcode == ABNF.OPCODE_CLOSE:
+            l = len(self.data)
+            if not l:
+                return
+            if l == 1 or l >= 126:
+                raise WebSocketProtocolException("Invalid close frame.")
+            if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]):
+                raise WebSocketProtocolException("Invalid close frame.")
+
+            code = 256 * \
+                six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2])
+            if not self._is_valid_close_status(code):
+                raise WebSocketProtocolException("Invalid close opcode.")
+
+    @staticmethod
+    def _is_valid_close_status(code):
+        return code in VALID_CLOSE_STATUS or (3000 <= code < 5000)
+
+    def __str__(self):
+        return "fin=" + str(self.fin) \
+            + " opcode=" + str(self.opcode) \
+            + " data=" + str(self.data)
+
+    @staticmethod
+    def create_frame(data, opcode, fin=1):
+        """
+        create frame to send text, binary and other data.
+
+        data: data to send. This is string value(byte array).
+            if opcode is OPCODE_TEXT and this value is unicode,
+            data value is converted into unicode string, automatically.
+
+        opcode: operation code. please see OPCODE_XXX.
+
+        fin: fin flag. if set to 0, create continue fragmentation.
+        """
+        if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type):
+            data = data.encode("utf-8")
+        # mask must be set if send data from client
+        return ABNF(fin, 0, 0, 0, opcode, 1, data)
+
+    def format(self):
+        """
+        format this object to string(byte array) to send data to server.
+        """
+        if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
+            raise ValueError("not 0 or 1")
+        if self.opcode not in ABNF.OPCODES:
+            raise ValueError("Invalid OPCODE")
+        length = len(self.data)
+        if length >= ABNF.LENGTH_63:
+            raise ValueError("data is too long")
+
+        frame_header = chr(self.fin << 7
+                           | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
+                           | self.opcode)
+        if length < ABNF.LENGTH_7:
+            frame_header += chr(self.mask << 7 | length)
+            frame_header = six.b(frame_header)
+        elif length < ABNF.LENGTH_16:
+            frame_header += chr(self.mask << 7 | 0x7e)
+            frame_header = six.b(frame_header)
+            frame_header += struct.pack("!H", length)
+        else:
+            frame_header += chr(self.mask << 7 | 0x7f)
+            frame_header = six.b(frame_header)
+            frame_header += struct.pack("!Q", length)
+
+        if not self.mask:
+            return frame_header + self.data
+        else:
+            mask_key = self.get_mask_key(4)
+            return frame_header + self._get_masked(mask_key)
+
+    def _get_masked(self, mask_key):
+        s = ABNF.mask(mask_key, self.data)
+
+        if isinstance(mask_key, six.text_type):
+            mask_key = mask_key.encode('utf-8')
+
+        return mask_key + s
+
+    @staticmethod
+    def mask(mask_key, data):
+        """
+        mask or unmask data. Just do xor for each byte
+
+        mask_key: 4 byte string(byte).
+
+        data: data to mask/unmask.
+        """
+        if data is None:
+            data = ""
+
+        if isinstance(mask_key, six.text_type):
+            mask_key = six.b(mask_key)
+
+        if isinstance(data, six.text_type):
+            data = six.b(data)
+
+        if numpy:
+            origlen = len(data)
+            _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0]
+
+            # We need data to be a multiple of four...
+            data += bytes(" " * (4 - (len(data) % 4)), "us-ascii")
+            a = numpy.frombuffer(data, dtype="uint32")
+            masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32")
+            if len(data) > origlen:
+              return masked.tobytes()[:origlen]
+            return masked.tobytes()
+        else:
+            _m = array.array("B", mask_key)
+            _d = array.array("B", data)
+            return _mask(_m, _d)
+
+
+class frame_buffer(object):
+    _HEADER_MASK_INDEX = 5
+    _HEADER_LENGTH_INDEX = 6
+
+    def __init__(self, recv_fn, skip_utf8_validation):
+        self.recv = recv_fn
+        self.skip_utf8_validation = skip_utf8_validation
+        # Buffers over the packets from the layer beneath until desired amount
+        # bytes of bytes are received.
+        self.recv_buffer = []
+        self.clear()
+        self.lock = Lock()
+
+    def clear(self):
+        self.header = None
+        self.length = None
+        self.mask = None
+
+    def has_received_header(self):
+        return self.header is None
+
+    def recv_header(self):
+        header = self.recv_strict(2)
+        b1 = header[0]
+
+        if six.PY2:
+            b1 = ord(b1)
+
+        fin = b1 >> 7 & 1
+        rsv1 = b1 >> 6 & 1
+        rsv2 = b1 >> 5 & 1
+        rsv3 = b1 >> 4 & 1
+        opcode = b1 & 0xf
+        b2 = header[1]
+
+        if six.PY2:
+            b2 = ord(b2)
+
+        has_mask = b2 >> 7 & 1
+        length_bits = b2 & 0x7f
+
+        self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits)
+
+    def has_mask(self):
+        if not self.header:
+            return False
+        return self.header[frame_buffer._HEADER_MASK_INDEX]
+
+    def has_received_length(self):
+        return self.length is None
+
+    def recv_length(self):
+        bits = self.header[frame_buffer._HEADER_LENGTH_INDEX]
+        length_bits = bits & 0x7f
+        if length_bits == 0x7e:
+            v = self.recv_strict(2)
+            self.length = struct.unpack("!H", v)[0]
+        elif length_bits == 0x7f:
+            v = self.recv_strict(8)
+            self.length = struct.unpack("!Q", v)[0]
+        else:
+            self.length = length_bits
+
+    def has_received_mask(self):
+        return self.mask is None
+
+    def recv_mask(self):
+        self.mask = self.recv_strict(4) if self.has_mask() else ""
+
+    def recv_frame(self):
+
+        with self.lock:
+            # Header
+            if self.has_received_header():
+                self.recv_header()
+            (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header
+
+            # Frame length
+            if self.has_received_length():
+                self.recv_length()
+            length = self.length
+
+            # Mask
+            if self.has_received_mask():
+                self.recv_mask()
+            mask = self.mask
+
+            # Payload
+            payload = self.recv_strict(length)
+            if has_mask:
+                payload = ABNF.mask(mask, payload)
+
+            # Reset for next frame
+            self.clear()
+
+            frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
+            frame.validate(self.skip_utf8_validation)
+
+        return frame
+
+    def recv_strict(self, bufsize):
+        shortage = bufsize - sum(len(x) for x in self.recv_buffer)
+        while shortage > 0:
+            # Limit buffer size that we pass to socket.recv() to avoid
+            # fragmenting the heap -- the number of bytes recv() actually
+            # reads is limited by socket buffer and is relatively small,
+            # yet passing large numbers repeatedly causes lots of large
+            # buffers allocated and then shrunk, which results in
+            # fragmentation.
+            bytes_ = self.recv(min(16384, shortage))
+            self.recv_buffer.append(bytes_)
+            shortage -= len(bytes_)
+
+        unified = six.b("").join(self.recv_buffer)
+
+        if shortage == 0:
+            self.recv_buffer = []
+            return unified
+        else:
+            self.recv_buffer = [unified[bufsize:]]
+            return unified[:bufsize]
+
+
+class continuous_frame(object):
+
+    def __init__(self, fire_cont_frame, skip_utf8_validation):
+        self.fire_cont_frame = fire_cont_frame
+        self.skip_utf8_validation = skip_utf8_validation
+        self.cont_data = None
+        self.recving_frames = None
+
+    def validate(self, frame):
+        if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT:
+            raise WebSocketProtocolException("Illegal frame")
+        if self.recving_frames and \
+                frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
+            raise WebSocketProtocolException("Illegal frame")
+
+    def add(self, frame):
+        if self.cont_data:
+            self.cont_data[1] += frame.data
+        else:
+            if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY):
+                self.recving_frames = frame.opcode
+            self.cont_data = [frame.opcode, frame.data]
+
+        if frame.fin:
+            self.recving_frames = None
+
+    def is_fire(self, frame):
+        return frame.fin or self.fire_cont_frame
+
+    def extract(self, frame):
+        data = self.cont_data
+        self.cont_data = None
+        frame.data = data[1]
+        if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data):
+            raise WebSocketPayloadException(
+                "cannot decode: " + repr(frame.data))
+
+        return [data[0], frame]

+ 325 - 0
mt/websocket/_app.py

@@ -0,0 +1,325 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+
+"""
+WebSocketApp provides higher level APIs.
+"""
+import select
+import sys
+import threading
+import time
+import traceback
+
+import six
+
+from ._abnf import ABNF
+from ._core import WebSocket, getdefaulttimeout
+from ._exceptions import *
+from . import _logging
+
+
+__all__ = ["WebSocketApp"]
+
+class Dispatcher:
+    def __init__(self, app, ping_timeout):
+        self.app  = app
+        self.ping_timeout = ping_timeout
+
+    def read(self, sock, read_callback, check_callback):
+        while self.app.sock.connected:
+            r, w, e = select.select(
+            (self.app.sock.sock, ), (), (), self.ping_timeout) # Use a 10 second timeout to avoid to wait forever on close
+            if r:
+                if not read_callback():
+                    break
+            check_callback()
+
+class SSLDispacther:
+    def __init__(self, app, ping_timeout):
+        self.app  = app
+        self.ping_timeout = ping_timeout
+
+    def read(self, sock, read_callback, check_callback):
+        while self.app.sock.connected:
+            r = self.select()
+            if r:
+                if not read_callback():
+                    break
+            check_callback()
+
+    def select(self):
+        sock = self.app.sock.sock
+        if sock.pending():
+            return [sock,]
+
+        r, w, e = select.select((sock, ), (), (), self.ping_timeout)
+        return r
+
+class WebSocketApp(object):
+    """
+    Higher level of APIs are provided.
+    The interface is like JavaScript WebSocket object.
+    """
+
+    def __init__(self, url, header=None,
+                 on_open=None, on_message=None, on_error=None,
+                 on_close=None, on_ping=None, on_pong=None,
+                 on_cont_message=None,
+                 keep_running=True, get_mask_key=None, cookie=None,
+                 subprotocols=None,
+                 on_data=None):
+        """
+        url: websocket url.
+        header: custom header for websocket handshake.
+        on_open: callable object which is called at opening websocket.
+          this function has one argument. The argument is this class object.
+        on_message: callable object which is called when received data.
+         on_message has 2 arguments.
+         The 1st argument is this class object.
+         The 2nd argument is utf-8 string which we get from the server.
+        on_error: callable object which is called when we get error.
+         on_error has 2 arguments.
+         The 1st argument is this class object.
+         The 2nd argument is exception object.
+        on_close: callable object which is called when closed the connection.
+         this function has one argument. The argument is this class object.
+        on_cont_message: callback object which is called when receive continued
+         frame data.
+         on_cont_message has 3 arguments.
+         The 1st argument is this class object.
+         The 2nd argument is utf-8 string which we get from the server.
+         The 3rd argument is continue flag. if 0, the data continue
+         to next frame data
+        on_data: callback object which is called when a message received.
+          This is called before on_message or on_cont_message,
+          and then on_message or on_cont_message is called.
+          on_data has 4 argument.
+          The 1st argument is this class object.
+          The 2nd argument is utf-8 string which we get from the server.
+          The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came.
+          The 4th argument is continue flag. if 0, the data continue
+        keep_running: this parameter is obosleted and ignored it.
+        get_mask_key: a callable to produce new mask keys,
+          see the WebSocket.set_mask_key's docstring for more information
+        subprotocols: array of available sub protocols. default is None.
+        """
+        self.url = url
+        self.header = header if header is not None else []
+        self.cookie = cookie
+        self.on_open = on_open
+        self.on_message = on_message
+        self.on_data = on_data
+        self.on_error = on_error
+        self.on_close = on_close
+        self.on_ping = on_ping
+        self.on_pong = on_pong
+        self.on_cont_message = on_cont_message
+        self.keep_running = False
+        self.get_mask_key = get_mask_key
+        self.sock = None
+        self.last_ping_tm = 0
+        self.last_pong_tm = 0
+        self.subprotocols = subprotocols
+
+    def send(self, data, opcode=ABNF.OPCODE_TEXT):
+        """
+        send message.
+        data: message to send. If you set opcode to OPCODE_TEXT,
+              data must be utf-8 string or unicode.
+        opcode: operation code of data. default is OPCODE_TEXT.
+        """
+
+        if not self.sock or self.sock.send(data, opcode) == 0:
+            raise WebSocketConnectionClosedException(
+                "Connection is already closed.")
+
+    def close(self, **kwargs):
+        """
+        close websocket connection.
+        """
+        self.keep_running = False
+        if self.sock:
+            self.sock.close(**kwargs)
+
+    def _send_ping(self, interval, event):
+        while not event.wait(interval):
+            self.last_ping_tm = time.time()
+            if self.sock:
+                try:
+                    self.sock.ping()
+                except Exception as ex:
+                    _logging.warning("send_ping routine terminated: {}".format(ex))
+                    break
+
+    def run_forever(self, sockopt=None, sslopt=None,
+                    ping_interval=0, ping_timeout=None,
+                    http_proxy_host=None, http_proxy_port=None,
+                    http_no_proxy=None, http_proxy_auth=None,
+                    skip_utf8_validation=False,
+                    host=None, origin=None, dispatcher=None):
+        """
+        run event loop for WebSocket framework.
+        This loop is infinite loop and is alive during websocket is available.
+        sockopt: values for socket.setsockopt.
+            sockopt must be tuple
+            and each element is argument of sock.setsockopt.
+        sslopt: ssl socket optional dict.
+        ping_interval: automatically send "ping" command
+            every specified period(second)
+            if set to 0, not send automatically.
+        ping_timeout: timeout(second) if the pong message is not received.
+        http_proxy_host: http proxy host name.
+        http_proxy_port: http proxy port. If not set, set to 80.
+        http_no_proxy: host names, which doesn't use proxy.
+        skip_utf8_validation: skip utf8 validation.
+        host: update host header.
+        origin: update origin header.
+        """
+
+        if not ping_timeout or ping_timeout <= 0:
+            ping_timeout = None
+        if ping_timeout and ping_interval and ping_interval <= ping_timeout:
+            raise WebSocketException("Ensure ping_interval > ping_timeout")
+        if sockopt is None:
+            sockopt = []
+        if sslopt is None:
+            sslopt = {}
+        if self.sock:
+            raise WebSocketException("socket is already opened")
+        thread = None
+        close_frame = None
+        self.keep_running = True
+        self.last_ping_tm = 0
+        self.last_pong_tm = 0
+
+        def teardown():
+            if thread and thread.isAlive():
+                event.set()
+                thread.join()
+            self.keep_running = False
+            self.sock.close()
+            close_args = self._get_close_args(
+                close_frame.data if close_frame else None)
+            self._callback(self.on_close, *close_args)
+            self.sock = None
+
+        try:
+            self.sock = WebSocket(
+                self.get_mask_key, sockopt=sockopt, sslopt=sslopt,
+                fire_cont_frame=self.on_cont_message and True or False,
+                skip_utf8_validation=skip_utf8_validation)
+            self.sock.settimeout(getdefaulttimeout())
+            self.sock.connect(
+                self.url, header=self.header, cookie=self.cookie,
+                http_proxy_host=http_proxy_host,
+                http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy,
+                http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols,
+                host=host, origin=origin)
+            if not dispatcher:
+                dispatcher = self.create_dispatcher(ping_timeout)
+
+            self._callback(self.on_open)
+
+            if ping_interval:
+                event = threading.Event()
+                thread = threading.Thread(
+                    target=self._send_ping, args=(ping_interval, event))
+                thread.setDaemon(True)
+                thread.start()
+
+            def read():
+                if not self.keep_running:
+                    return teardown()
+
+                op_code, frame = self.sock.recv_data_frame(True)
+                if op_code == ABNF.OPCODE_CLOSE:
+                    close_frame = frame
+                    return teardown()
+                elif op_code == ABNF.OPCODE_PING:
+                    self._callback(self.on_ping, frame.data)
+                elif op_code == ABNF.OPCODE_PONG:
+                    self.last_pong_tm = time.time()
+                    self._callback(self.on_pong, frame.data)
+                elif op_code == ABNF.OPCODE_CONT and self.on_cont_message:
+                    self._callback(self.on_data, frame.data,
+                                   frame.opcode, frame.fin)
+                    self._callback(self.on_cont_message,
+                                   frame.data, frame.fin)
+                else:
+                    data = frame.data
+                    if six.PY3 and op_code == ABNF.OPCODE_TEXT:
+                        data = data.decode("utf-8")
+                    self._callback(self.on_data, data, frame.opcode, True)
+                    self._callback(self.on_message, data)
+
+                return True
+
+            def check():
+                if ping_timeout and self.last_ping_tm \
+                        and time.time() - self.last_ping_tm > ping_timeout \
+                        and self.last_ping_tm - self.last_pong_tm > ping_timeout:
+                    raise WebSocketTimeoutException("ping/pong timed out")
+                return True
+
+            dispatcher.read(self.sock.sock, read, check)
+        except (Exception, KeyboardInterrupt, SystemExit) as e:
+            self._callback(self.on_error, e)
+            if isinstance(e, SystemExit):
+                # propagate SystemExit further
+                raise
+            teardown()
+
+    def create_dispatcher(self, ping_timeout):
+        timeout = ping_timeout or 10
+        if self.sock.is_ssl():
+            return SSLDispacther(self, timeout)
+
+        return Dispatcher(self, timeout)
+
+    def _get_close_args(self, data):
+        """ this functions extracts the code, reason from the close body
+        if they exists, and if the self.on_close except three arguments """
+        import inspect
+        # if the on_close callback is "old", just return empty list
+        if sys.version_info < (3, 0):
+            if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3:
+                return []
+        else:
+            if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3:
+                return []
+
+        if data and len(data) >= 2:
+            code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2])
+            reason = data[2:].decode('utf-8')
+            return [code, reason]
+
+        return [None, None]
+
+    def _callback(self, callback, *args):
+        if callback:
+            try:
+                callback(self, *args)
+            except Exception as e:
+                _logging.error("error from callback {}: {}".format(callback, e))
+                if _logging.isEnabledForDebug():
+                    _, _, tb = sys.exc_info()
+                    traceback.print_tb(tb)

+ 52 - 0
mt/websocket/_cookiejar.py

@@ -0,0 +1,52 @@
+try:
+    import Cookie
+except:
+    import http.cookies as Cookie
+
+
+class SimpleCookieJar(object):
+    def __init__(self):
+        self.jar = dict()
+
+    def add(self, set_cookie):
+        if set_cookie:
+            try:
+                simpleCookie = Cookie.SimpleCookie(set_cookie)
+            except:
+                simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
+
+            for k, v in simpleCookie.items():
+                domain = v.get("domain")
+                if domain:
+                    if not domain.startswith("."):
+                        domain = "." + domain
+                    cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie()
+                    cookie.update(simpleCookie)
+                    self.jar[domain.lower()] = cookie
+
+    def set(self, set_cookie):
+        if set_cookie:
+            try:
+                simpleCookie = Cookie.SimpleCookie(set_cookie)
+            except:
+                simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore'))
+
+            for k, v in simpleCookie.items():
+                domain = v.get("domain")
+                if domain:
+                    if not domain.startswith("."):
+                        domain = "." + domain
+                    self.jar[domain.lower()] = simpleCookie
+
+    def get(self, host):
+        if not host:
+            return ""
+
+        cookies = []
+        for domain, simpleCookie in self.jar.items():
+            host = host.lower()
+            if host.endswith(domain) or host == domain[1:]:
+                cookies.append(self.jar.get(domain))
+
+        return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in
+                                       sorted(cookie.items())]))

+ 495 - 0
mt/websocket/_core.py

@@ -0,0 +1,495 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+from __future__ import print_function
+
+import socket
+import struct
+import threading
+
+import six
+
+# websocket modules
+from ._abnf import *
+from ._exceptions import *
+from ._handshake import *
+from ._http import *
+from ._logging import *
+from ._socket import *
+from ._ssl_compat import *
+from ._utils import *
+
+__all__ = ['WebSocket', 'create_connection']
+
+"""
+websocket python client.
+=========================
+
+This version support only hybi-13.
+Please see http://tools.ietf.org/html/rfc6455 for protocol.
+"""
+
+
+class WebSocket(object):
+    """
+    Low level WebSocket interface.
+    This class is based on
+      The WebSocket protocol draft-hixie-thewebsocketprotocol-76
+      http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+
+    We can connect to the websocket server and send/receive data.
+    The following example is an echo client.
+
+    >>> import websocket
+    >>> ws = websocket.WebSocket()
+    >>> ws.connect("ws://echo.websocket.org")
+    >>> ws.send("Hello, Server")
+    >>> ws.recv()
+    'Hello, Server'
+    >>> ws.close()
+
+    get_mask_key: a callable to produce new mask keys, see the set_mask_key
+      function's docstring for more details
+    sockopt: values for socket.setsockopt.
+        sockopt must be tuple and each element is argument of sock.setsockopt.
+    sslopt: dict object for ssl socket option.
+    fire_cont_frame: fire recv event for each cont frame. default is False
+    enable_multithread: if set to True, lock send method.
+    skip_utf8_validation: skip utf8 validation.
+    """
+
+    def __init__(self, get_mask_key=None, sockopt=None, sslopt=None,
+                 fire_cont_frame=False, enable_multithread=False,
+                 skip_utf8_validation=False, **_):
+        """
+        Initialize WebSocket object.
+        """
+        self.sock_opt = sock_opt(sockopt, sslopt)
+        self.handshake_response = None
+        self.sock = None
+
+        self.connected = False
+        self.get_mask_key = get_mask_key
+        # These buffer over the build-up of a single frame.
+        self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation)
+        self.cont_frame = continuous_frame(
+            fire_cont_frame, skip_utf8_validation)
+
+        if enable_multithread:
+            self.lock = threading.Lock()
+            self.readlock = threading.Lock()
+        else:
+            self.lock = NoLock()
+            self.readlock = NoLock()
+
+    def __iter__(self):
+        """
+        Allow iteration over websocket, implying sequential `recv` executions.
+        """
+        while True:
+            yield self.recv()
+
+    def __next__(self):
+        return self.recv()
+
+    def next(self):
+        return self.__next__()
+
+    def fileno(self):
+        return self.sock.fileno()
+
+    def set_mask_key(self, func):
+        """
+        set function to create musk key. You can customize mask key generator.
+        Mainly, this is for testing purpose.
+
+        func: callable object. the func takes 1 argument as integer.
+              The argument means length of mask key.
+              This func must return string(byte array),
+              which length is argument specified.
+        """
+        self.get_mask_key = func
+
+    def gettimeout(self):
+        """
+        Get the websocket timeout(second).
+        """
+        return self.sock_opt.timeout
+
+    def settimeout(self, timeout):
+        """
+        Set the timeout to the websocket.
+
+        timeout: timeout time(second).
+        """
+        self.sock_opt.timeout = timeout
+        if self.sock:
+            self.sock.settimeout(timeout)
+
+    timeout = property(gettimeout, settimeout)
+
+    def getsubprotocol(self):
+        """
+        get subprotocol
+        """
+        if self.handshake_response:
+            return self.handshake_response.subprotocol
+        else:
+            return None
+
+    subprotocol = property(getsubprotocol)
+
+    def getstatus(self):
+        """
+        get handshake status
+        """
+        if self.handshake_response:
+            return self.handshake_response.status
+        else:
+            return None
+
+    status = property(getstatus)
+
+    def getheaders(self):
+        """
+        get handshake response header
+        """
+        if self.handshake_response:
+            return self.handshake_response.headers
+        else:
+            return None
+
+    def is_ssl(self):
+        return isinstance(self.sock, ssl.SSLSocket)
+
+    headers = property(getheaders)
+
+    def connect(self, url, **options):
+        """
+        Connect to url. url is websocket url scheme.
+        ie. ws://host:port/resource
+        You can customize using 'options'.
+        If you set "header" list object, you can set your own custom header.
+
+        >>> ws = WebSocket()
+        >>> ws.connect("ws://echo.websocket.org/",
+                ...     header=["User-Agent: MyProgram",
+                ...             "x-custom: header"])
+
+        timeout: socket timeout time. This value is integer.
+                 if you set None for this value,
+                 it means "use default_timeout value"
+
+        options: "header" -> custom http header list or dict.
+                 "cookie" -> cookie value.
+                 "origin" -> custom origin url.
+                 "host"   -> custom host header string.
+                 "http_proxy_host" - http proxy host name.
+                 "http_proxy_port" - http proxy port. If not set, set to 80.
+                 "http_no_proxy"   - host names, which doesn't use proxy.
+                 "http_proxy_auth" - http proxy auth information.
+                                     tuple of username and password.
+                                     default is None
+                 "subprotocols" - array of available sub protocols.
+                                  default is None.
+                 "socket" - pre-initialized stream socket.
+
+        """
+        self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options),
+                                   options.pop('socket', None))
+
+        try:
+            self.handshake_response = handshake(self.sock, *addrs, **options)
+            self.connected = True
+        except:
+            if self.sock:
+                self.sock.close()
+                self.sock = None
+            raise
+
+    def send(self, payload, opcode=ABNF.OPCODE_TEXT):
+        """
+        Send the data as string.
+
+        payload: Payload must be utf-8 string or unicode,
+                  if the opcode is OPCODE_TEXT.
+                  Otherwise, it must be string(byte array)
+
+        opcode: operation code to send. Please see OPCODE_XXX.
+        """
+
+        frame = ABNF.create_frame(payload, opcode)
+        return self.send_frame(frame)
+
+    def send_frame(self, frame):
+        """
+        Send the data frame.
+
+        frame: frame data created  by ABNF.create_frame
+
+        >>> ws = create_connection("ws://echo.websocket.org/")
+        >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT)
+        >>> ws.send_frame(frame)
+        >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0)
+        >>> ws.send_frame(frame)
+        >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1)
+        >>> ws.send_frame(frame)
+
+        """
+        if self.get_mask_key:
+            frame.get_mask_key = self.get_mask_key
+        data = frame.format()
+        length = len(data)
+        trace("send: " + repr(data))
+
+        with self.lock:
+            while data:
+                l = self._send(data)
+                data = data[l:]
+
+        return length
+
+    def send_binary(self, payload):
+        return self.send(payload, ABNF.OPCODE_BINARY)
+
+    def ping(self, payload=""):
+        """
+        send ping data.
+
+        payload: data payload to send server.
+        """
+        if isinstance(payload, six.text_type):
+            payload = payload.encode("utf-8")
+        self.send(payload, ABNF.OPCODE_PING)
+
+    def pong(self, payload):
+        """
+        send pong data.
+
+        payload: data payload to send server.
+        """
+        if isinstance(payload, six.text_type):
+            payload = payload.encode("utf-8")
+        self.send(payload, ABNF.OPCODE_PONG)
+
+    def recv(self):
+        """
+        Receive string data(byte array) from the server.
+
+        return value: string(byte array) value.
+        """
+        with self.readlock:
+            opcode, data = self.recv_data()
+        if six.PY3 and opcode == ABNF.OPCODE_TEXT:
+            return data.decode("utf-8")
+        elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY:
+            return data
+        else:
+            return ''
+
+    def recv_data(self, control_frame=False):
+        """
+        Receive data with operation code.
+
+        control_frame: a boolean flag indicating whether to return control frame
+        data, defaults to False
+
+        return  value: tuple of operation code and string(byte array) value.
+        """
+        opcode, frame = self.recv_data_frame(control_frame)
+        return opcode, frame.data
+
+    def recv_data_frame(self, control_frame=False):
+        """
+        Receive data with operation code.
+
+        control_frame: a boolean flag indicating whether to return control frame
+        data, defaults to False
+
+        return  value: tuple of operation code and string(byte array) value.
+        """
+        while True:
+            frame = self.recv_frame()
+            if not frame:
+                # handle error:
+                # 'NoneType' object has no attribute 'opcode'
+                raise WebSocketProtocolException(
+                    "Not a valid frame %s" % frame)
+            elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
+                self.cont_frame.validate(frame)
+                self.cont_frame.add(frame)
+
+                if self.cont_frame.is_fire(frame):
+                    return self.cont_frame.extract(frame)
+
+            elif frame.opcode == ABNF.OPCODE_CLOSE:
+                self.send_close()
+                return frame.opcode, frame
+            elif frame.opcode == ABNF.OPCODE_PING:
+                if len(frame.data) < 126:
+                    self.pong(frame.data)
+                else:
+                    raise WebSocketProtocolException(
+                        "Ping message is too long")
+                if control_frame:
+                    return frame.opcode, frame
+            elif frame.opcode == ABNF.OPCODE_PONG:
+                if control_frame:
+                    return frame.opcode, frame
+
+    def recv_frame(self):
+        """
+        receive data as frame from server.
+
+        return value: ABNF frame object.
+        """
+        return self.frame_buffer.recv_frame()
+
+    def send_close(self, status=STATUS_NORMAL, reason=six.b("")):
+        """
+        send close data to the server.
+
+        status: status code to send. see STATUS_XXX.
+
+        reason: the reason to close. This must be string or bytes.
+        """
+        if status < 0 or status >= ABNF.LENGTH_16:
+            raise ValueError("code is invalid range")
+        self.connected = False
+        self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
+
+    def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3):
+        """
+        Close Websocket object
+
+        status: status code to send. see STATUS_XXX.
+
+        reason: the reason to close. This must be string.
+
+        timeout: timeout until receive a close frame.
+            If None, it will wait forever until receive a close frame.
+        """
+        if self.connected:
+            if status < 0 or status >= ABNF.LENGTH_16:
+                raise ValueError("code is invalid range")
+
+            try:
+                self.connected = False
+                self.send(struct.pack('!H', status) +
+                          reason, ABNF.OPCODE_CLOSE)
+                sock_timeout = self.sock.gettimeout()
+                self.sock.settimeout(timeout)
+                try:
+                    frame = self.recv_frame()
+                    if isEnabledForError():
+                        recv_status = struct.unpack("!H", frame.data[0:2])[0]
+                        if recv_status != STATUS_NORMAL:
+                            error("close status: " + repr(recv_status))
+                except:
+                    pass
+                self.sock.settimeout(sock_timeout)
+                self.sock.shutdown(socket.SHUT_RDWR)
+            except:
+                pass
+
+        self.shutdown()
+
+    def abort(self):
+        """
+        Low-level asynchronous abort, wakes up other threads that are waiting in recv_*
+        """
+        if self.connected:
+            self.sock.shutdown(socket.SHUT_RDWR)
+
+    def shutdown(self):
+        """close socket, immediately."""
+        if self.sock:
+            self.sock.close()
+            self.sock = None
+            self.connected = False
+
+    def _send(self, data):
+        return send(self.sock, data)
+
+    def _recv(self, bufsize):
+        try:
+            return recv(self.sock, bufsize)
+        except WebSocketConnectionClosedException:
+            if self.sock:
+                self.sock.close()
+            self.sock = None
+            self.connected = False
+            raise
+
+
+def create_connection(url, timeout=None, class_=WebSocket, **options):
+    """
+    connect to url and return websocket object.
+
+    Connect to url and return the WebSocket object.
+    Passing optional timeout parameter will set the timeout on the socket.
+    If no timeout is supplied,
+    the global default timeout setting returned by getdefauttimeout() is used.
+    You can customize using 'options'.
+    If you set "header" list object, you can set your own custom header.
+
+    >>> conn = create_connection("ws://echo.websocket.org/",
+         ...     header=["User-Agent: MyProgram",
+         ...             "x-custom: header"])
+
+
+    timeout: socket timeout time. This value is integer.
+             if you set None for this value,
+             it means "use default_timeout value"
+
+    class_: class to instantiate when creating the connection. It has to implement
+            settimeout and connect. It's __init__ should be compatible with
+            WebSocket.__init__, i.e. accept all of it's kwargs.
+    options: "header" -> custom http header list or dict.
+             "cookie" -> cookie value.
+             "origin" -> custom origin url.
+             "host"   -> custom host header string.
+             "http_proxy_host" - http proxy host name.
+             "http_proxy_port" - http proxy port. If not set, set to 80.
+             "http_no_proxy"   - host names, which doesn't use proxy.
+             "http_proxy_auth" - http proxy auth information.
+                                    tuple of username and password.
+                                    default is None
+             "enable_multithread" -> enable lock for multithread.
+             "sockopt" -> socket options
+             "sslopt" -> ssl option
+             "subprotocols" - array of available sub protocols.
+                              default is None.
+             "skip_utf8_validation" - skip utf8 validation.
+             "socket" - pre-initialized stream socket.
+    """
+    sockopt = options.pop("sockopt", [])
+    sslopt = options.pop("sslopt", {})
+    fire_cont_frame = options.pop("fire_cont_frame", False)
+    enable_multithread = options.pop("enable_multithread", False)
+    skip_utf8_validation = options.pop("skip_utf8_validation", False)
+    websock = class_(sockopt=sockopt, sslopt=sslopt,
+                     fire_cont_frame=fire_cont_frame,
+                     enable_multithread=enable_multithread,
+                     skip_utf8_validation=skip_utf8_validation, **options)
+    websock.settimeout(timeout if timeout is not None else getdefaulttimeout())
+    websock.connect(url, **options)
+    return websock

+ 87 - 0
mt/websocket/_exceptions.py

@@ -0,0 +1,87 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+
+
+"""
+define websocket exceptions
+"""
+
+
+class WebSocketException(Exception):
+    """
+    websocket exception class.
+    """
+    pass
+
+
+class WebSocketProtocolException(WebSocketException):
+    """
+    If the websocket protocol is invalid, this exception will be raised.
+    """
+    pass
+
+
+class WebSocketPayloadException(WebSocketException):
+    """
+    If the websocket payload is invalid, this exception will be raised.
+    """
+    pass
+
+
+class WebSocketConnectionClosedException(WebSocketException):
+    """
+    If remote host closed the connection or some network error happened,
+    this exception will be raised.
+    """
+    pass
+
+
+class WebSocketTimeoutException(WebSocketException):
+    """
+    WebSocketTimeoutException will be raised at socket timeout during read/write data.
+    """
+    pass
+
+
+class WebSocketProxyException(WebSocketException):
+    """
+    WebSocketProxyException will be raised when proxy error occurred.
+    """
+    pass
+
+
+class WebSocketBadStatusException(WebSocketException):
+    """
+    WebSocketBadStatusException will be raised when we get bad handshake status code.
+    """
+
+    def __init__(self, message, status_code, status_message=None):
+        msg = message % (status_code, status_message) if status_message is not None \
+            else  message % status_code
+        super(WebSocketBadStatusException, self).__init__(msg)
+        self.status_code = status_code
+
+class WebSocketAddressException(WebSocketException):
+    """
+    If the websocket address info cannot be found, this exception will be raised.
+    """
+    pass

+ 180 - 0
mt/websocket/_handshake.py

@@ -0,0 +1,180 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+import hashlib
+import hmac
+import os
+
+import six
+
+from ._cookiejar import SimpleCookieJar
+from ._exceptions import *
+from ._http import *
+from ._logging import *
+from ._socket import *
+
+if six.PY3:
+    from base64 import encodebytes as base64encode
+else:
+    from base64 import encodestring as base64encode
+
+__all__ = ["handshake_response", "handshake"]
+
+if hasattr(hmac, "compare_digest"):
+    compare_digest = hmac.compare_digest
+else:
+    def compare_digest(s1, s2):
+        return s1 == s2
+
+# websocket supported version.
+VERSION = 13
+
+CookieJar = SimpleCookieJar()
+
+
+class handshake_response(object):
+
+    def __init__(self, status, headers, subprotocol):
+        self.status = status
+        self.headers = headers
+        self.subprotocol = subprotocol
+        CookieJar.add(headers.get("set-cookie"))
+
+
+def handshake(sock, hostname, port, resource, **options):
+    headers, key = _get_handshake_headers(resource, hostname, port, options)
+
+    header_str = "\r\n".join(headers)
+    send(sock, header_str)
+    dump("request header", header_str)
+
+    status, resp = _get_resp_headers(sock)
+    success, subproto = _validate(resp, key, options.get("subprotocols"))
+    if not success:
+        raise WebSocketException("Invalid WebSocket Header")
+
+    return handshake_response(status, resp, subproto)
+
+def _pack_hostname(hostname):
+    # IPv6 address
+    if ':' in hostname:
+        return '[' + hostname + ']'
+
+    return hostname
+
+def _get_handshake_headers(resource, host, port, options):
+    headers = [
+        "GET %s HTTP/1.1" % resource,
+        "Upgrade: websocket",
+        "Connection: Upgrade"
+    ]
+    if port == 80 or port == 443:
+        hostport = _pack_hostname(host)
+    else:
+        hostport = "%s:%d" % (_pack_hostname(host), port)
+
+    if "host" in options and options["host"] is not None:
+        headers.append("Host: %s" % options["host"])
+    else:
+        headers.append("Host: %s" % hostport)
+
+    if "origin" in options and options["origin"] is not None:
+        headers.append("Origin: %s" % options["origin"])
+    else:
+        headers.append("Origin: http://%s" % hostport)
+
+    key = _create_sec_websocket_key()
+    headers.append("Sec-WebSocket-Key: %s" % key)
+    headers.append("Sec-WebSocket-Version: %s" % VERSION)
+
+    subprotocols = options.get("subprotocols")
+    if subprotocols:
+        headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols))
+
+    if "header" in options:
+        header = options["header"]
+        if isinstance(header, dict):
+            header = map(": ".join, header.items())
+        headers.extend(header)
+
+    server_cookie = CookieJar.get(host)
+    client_cookie = options.get("cookie", None)
+
+    cookie = "; ".join(filter(None, [server_cookie, client_cookie]))
+
+    if cookie:
+        headers.append("Cookie: %s" % cookie)
+
+    headers.append("")
+    headers.append("")
+
+    return headers, key
+
+
+def _get_resp_headers(sock, success_status=101):
+    status, resp_headers, status_message = read_headers(sock)
+    if status != success_status:
+        raise WebSocketBadStatusException("Handshake status %d %s", status, status_message)
+    return status, resp_headers
+
+_HEADERS_TO_CHECK = {
+    "upgrade": "websocket",
+    "connection": "upgrade",
+}
+
+
+def _validate(headers, key, subprotocols):
+    subproto = None
+    for k, v in _HEADERS_TO_CHECK.items():
+        r = headers.get(k, None)
+        if not r:
+            return False, None
+        r = r.lower()
+        if v != r:
+            return False, None
+
+    if subprotocols:
+        subproto = headers.get("sec-websocket-protocol", None).lower()
+        if not subproto or subproto not in [s.lower() for s in subprotocols]:
+            error("Invalid subprotocol: " + str(subprotocols))
+            return False, None
+
+    result = headers.get("sec-websocket-accept", None)
+    if not result:
+        return False, None
+    result = result.lower()
+
+    if isinstance(result, six.text_type):
+        result = result.encode('utf-8')
+
+    value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8')
+    hashed = base64encode(hashlib.sha1(value).digest()).strip().lower()
+    success = compare_digest(hashed, result)
+
+    if success:
+        return True, subproto
+    else:
+        return False, None
+
+
+def _create_sec_websocket_key():
+    randomness = os.urandom(16)
+    return base64encode(randomness).decode('utf-8').strip()

+ 319 - 0
mt/websocket/_http.py

@@ -0,0 +1,319 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+import errno
+import os
+import socket
+import sys
+
+import six
+
+from ._exceptions import *
+from ._logging import *
+from ._socket import*
+from ._ssl_compat import *
+from ._url import *
+
+if six.PY3:
+    from base64 import encodebytes as base64encode
+else:
+    from base64 import encodestring as base64encode
+
+__all__ = ["proxy_info", "connect", "read_headers"]
+
+try:
+    import socks
+    ProxyConnectionError = socks.ProxyConnectionError
+    HAS_PYSOCKS = True
+except:
+    class ProxyConnectionError(BaseException):
+        pass
+    HAS_PYSOCKS = False
+
+class proxy_info(object):
+
+    def __init__(self, **options):
+        self.type = options.get("proxy_type", "http")
+        if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']):
+            raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'")
+        self.host = options.get("http_proxy_host", None)
+        if self.host:
+            self.port = options.get("http_proxy_port", 0)
+            self.auth = options.get("http_proxy_auth", None)
+            self.no_proxy = options.get("http_no_proxy", None)
+        else:
+            self.port = 0
+            self.auth = None
+            self.no_proxy = None
+
+def _open_proxied_socket(url, options, proxy):
+    hostname, port, resource, is_secure = parse_url(url)
+
+    if not HAS_PYSOCKS:
+        raise WebSocketException("PySocks module not found.")
+
+    ptype = socks.SOCKS5
+    rdns = False
+    if proxy.type == "socks4":
+        ptype = socks.SOCKS4
+    if proxy.type == "http":
+        ptype = socks.HTTP
+    if proxy.type[-1] == "h":
+        rdns = True
+
+    sock = socks.create_connection(
+            (hostname, port),
+            proxy_type = ptype,
+            proxy_addr = proxy.host,
+            proxy_port = proxy.port,
+            proxy_rdns = rdns,
+            proxy_username = proxy.auth[0] if proxy.auth else None,
+            proxy_password = proxy.auth[1] if proxy.auth else None,
+            timeout = options.timeout,
+            socket_options = DEFAULT_SOCKET_OPTION + options.sockopt
+    )
+
+    if is_secure:
+        if HAVE_SSL:
+            sock = _ssl_socket(sock, options.sslopt, hostname)
+        else:
+            raise WebSocketException("SSL not available.")
+
+    return sock, (hostname, port, resource)
+
+
+def connect(url, options, proxy, socket):
+    if proxy.host and not socket and not(proxy.type == 'http'):
+        return _open_proxied_socket(url, options, proxy)
+
+    hostname, port, resource, is_secure = parse_url(url)
+
+    if socket:
+        return socket, (hostname, port, resource)
+
+    addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
+        hostname, port, is_secure, proxy)
+    if not addrinfo_list:
+        raise WebSocketException(
+            "Host not found.: " + hostname + ":" + str(port))
+
+    sock = None
+    try:
+        sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
+        if need_tunnel:
+            sock = _tunnel(sock, hostname, port, auth)
+
+        if is_secure:
+            if HAVE_SSL:
+                sock = _ssl_socket(sock, options.sslopt, hostname)
+            else:
+                raise WebSocketException("SSL not available.")
+
+        return sock, (hostname, port, resource)
+    except:
+        if sock:
+            sock.close()
+        raise
+
+
+def _get_addrinfo_list(hostname, port, is_secure, proxy):
+    phost, pport, pauth = get_proxy_info(
+        hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy)
+    try:
+        if not phost:
+            addrinfo_list = socket.getaddrinfo(
+                hostname, port, 0, 0, socket.SOL_TCP)
+            return addrinfo_list, False, None
+        else:
+            pport = pport and pport or 80
+            # when running on windows 10, the getaddrinfo used above
+            # returns a socktype 0. This generates an error exception:
+            #_on_error: exception Socket type must be stream or datagram, not 0
+            # Force the socket type to SOCK_STREAM
+            addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
+            return addrinfo_list, True, pauth
+    except socket.gaierror as e:
+        raise WebSocketAddressException(e)
+
+
+def _open_socket(addrinfo_list, sockopt, timeout):
+    err = None
+    for addrinfo in addrinfo_list:
+        family, socktype, proto = addrinfo[:3]
+        sock = socket.socket(family, socktype, proto)
+        sock.settimeout(timeout)
+        for opts in DEFAULT_SOCKET_OPTION:
+            sock.setsockopt(*opts)
+        for opts in sockopt:
+            sock.setsockopt(*opts)
+
+        address = addrinfo[4]
+        try:
+            sock.connect(address)
+            err = None
+        except ProxyConnectionError as error:
+            err = WebSocketProxyException(str(error))
+            err.remote_ip = str(address[0])
+            continue
+        except socket.error as error:
+            error.remote_ip = str(address[0])
+            try:
+                eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)
+            except:
+                eConnRefused = (errno.ECONNREFUSED, )
+            if error.errno in eConnRefused:
+                err = error
+                continue
+            else:
+                raise error
+        else:
+            break
+    else:
+        raise err
+
+    return sock
+
+
+def _can_use_sni():
+    return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2)
+
+
+def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
+    context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
+
+    if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
+        cafile = sslopt.get('ca_certs', None)
+        capath = sslopt.get('ca_cert_path', None)
+        if cafile or capath:
+            context.load_verify_locations(cafile=cafile, capath=capath)
+        elif hasattr(context, 'load_default_certs'):
+            context.load_default_certs(ssl.Purpose.SERVER_AUTH)
+    if sslopt.get('certfile', None):
+        context.load_cert_chain(
+            sslopt['certfile'],
+            sslopt.get('keyfile', None),
+            sslopt.get('password', None),
+        )
+    # see
+    # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
+    context.verify_mode = sslopt['cert_reqs']
+    if HAVE_CONTEXT_CHECK_HOSTNAME:
+        context.check_hostname = check_hostname
+    if 'ciphers' in sslopt:
+        context.set_ciphers(sslopt['ciphers'])
+    if 'cert_chain' in sslopt:
+        certfile, keyfile, password = sslopt['cert_chain']
+        context.load_cert_chain(certfile, keyfile, password)
+    if 'ecdh_curve' in sslopt:
+        context.set_ecdh_curve(sslopt['ecdh_curve'])
+
+    return context.wrap_socket(
+        sock,
+        do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
+        suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
+        server_hostname=hostname,
+    )
+
+
+def _ssl_socket(sock, user_sslopt, hostname):
+    sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
+    sslopt.update(user_sslopt)
+
+    certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
+    if certPath and os.path.isfile(certPath) \
+            and user_sslopt.get('ca_certs', None) is None \
+            and user_sslopt.get('ca_cert', None) is None:
+        sslopt['ca_certs'] = certPath
+    elif certPath and os.path.isdir(certPath) \
+            and user_sslopt.get('ca_cert_path', None) is None:
+        sslopt['ca_cert_path'] = certPath
+
+    check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop(
+        'check_hostname', True)
+
+    if _can_use_sni():
+        sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
+    else:
+        sslopt.pop('check_hostname', True)
+        sock = ssl.wrap_socket(sock, **sslopt)
+
+    if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:
+        match_hostname(sock.getpeercert(), hostname)
+
+    return sock
+
+
+def _tunnel(sock, host, port, auth):
+    debug("Connecting proxy...")
+    connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port)
+    # TODO: support digest auth.
+    if auth and auth[0]:
+        auth_str = auth[0]
+        if auth[1]:
+            auth_str += ":" + auth[1]
+        encoded_str = base64encode(auth_str.encode()).strip().decode()
+        connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str
+    connect_header += "\r\n"
+    dump("request header", connect_header)
+
+    send(sock, connect_header)
+
+    try:
+        status, resp_headers, status_message = read_headers(sock)
+    except Exception as e:
+        raise WebSocketProxyException(str(e))
+
+    if status != 200:
+        raise WebSocketProxyException(
+            "failed CONNECT via proxy status: %r" % status)
+
+    return sock
+
+
+def read_headers(sock):
+    status = None
+    status_message = None
+    headers = {}
+    trace("--- response header ---")
+
+    while True:
+        line = recv_line(sock)
+        line = line.decode('utf-8').strip()
+        if not line:
+            break
+        trace(line)
+        if not status:
+
+            status_info = line.split(" ", 2)
+            status = int(status_info[1])
+            if len(status_info) > 2:
+                status_message = status_info[2]
+        else:
+            kv = line.split(":", 1)
+            if len(kv) == 2:
+                key, value = kv
+                headers[key.lower()] = value.strip()
+            else:
+                raise WebSocketException("Invalid header")
+
+    trace("-----------------------")
+
+    return status, headers, status_message

+ 75 - 0
mt/websocket/_logging.py

@@ -0,0 +1,75 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+import logging
+
+_logger = logging.getLogger('websocket')
+_logger.addHandler(logging.NullHandler())
+_traceEnabled = False
+
+__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace",
+           "isEnabledForError", "isEnabledForDebug"]
+
+
+def enableTrace(traceable):
+    """
+    turn on/off the traceability.
+
+    traceable: boolean value. if set True, traceability is enabled.
+    """
+    global _traceEnabled
+    _traceEnabled = traceable
+    if traceable:
+        if not _logger.handlers:
+            _logger.addHandler(logging.StreamHandler())
+        _logger.setLevel(logging.DEBUG)
+
+
+def dump(title, message):
+    if _traceEnabled:
+        _logger.debug("--- " + title + " ---")
+        _logger.debug(message)
+        _logger.debug("-----------------------")
+
+
+def error(msg):
+    _logger.error(msg)
+
+
+def warning(msg):
+    _logger.warning(msg)
+
+
+def debug(msg):
+    _logger.debug(msg)
+
+
+def trace(msg):
+    if _traceEnabled:
+        _logger.debug(msg)
+
+
+def isEnabledForError():
+    return _logger.isEnabledFor(logging.ERROR)
+
+
+def isEnabledForDebug():
+    return _logger.isEnabledFor(logging.DEBUG)

+ 126 - 0
mt/websocket/_socket.py

@@ -0,0 +1,126 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1335  USA
+
+"""
+import socket
+
+import six
+import sys
+
+from ._exceptions import *
+from ._ssl_compat import *
+from ._utils import *
+
+DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)]
+if hasattr(socket, "SO_KEEPALIVE"):
+    DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1))
+if hasattr(socket, "TCP_KEEPIDLE"):
+    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30))
+if hasattr(socket, "TCP_KEEPINTVL"):
+    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10))
+if hasattr(socket, "TCP_KEEPCNT"):
+    DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3))
+
+_default_timeout = None
+
+__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout",
+           "recv", "recv_line", "send"]
+
+
+class sock_opt(object):
+
+    def __init__(self, sockopt, sslopt):
+        if sockopt is None:
+            sockopt = []
+        if sslopt is None:
+            sslopt = {}
+        self.sockopt = sockopt
+        self.sslopt = sslopt
+        self.timeout = None
+
+
+def setdefaulttimeout(timeout):
+    """
+    Set the global timeout setting to connect.
+
+    timeout: default socket timeout time. This value is second.
+    """
+    global _default_timeout
+    _default_timeout = timeout
+
+
+def getdefaulttimeout():
+    """
+    Return the global timeout setting(second) to connect.
+    """
+    return _default_timeout
+
+
+def recv(sock, bufsize):
+    if not sock:
+        raise WebSocketConnectionClosedException("socket is already closed.")
+
+    try:
+        bytes_ = sock.recv(bufsize)
+    except socket.timeout as e:
+        message = extract_err_message(e)
+        raise WebSocketTimeoutException(message)
+    except SSLError as e:
+        message = extract_err_message(e)
+        if isinstance(message, str) and 'timed out' in message:
+            raise WebSocketTimeoutException(message)
+        else:
+            raise
+
+    if not bytes_:
+        raise WebSocketConnectionClosedException(
+            "Connection is already closed.")
+
+    return bytes_
+
+
+def recv_line(sock):
+    line = []
+    while True:
+        c = recv(sock, 1)
+        line.append(c)
+        if c == six.b("\n"):
+            break
+    return six.b("").join(line)
+
+
+def send(sock, data):
+    if isinstance(data, six.text_type):
+        data = data.encode('utf-8')
+
+    if not sock:
+        raise WebSocketConnectionClosedException("socket is already closed.")
+
+    try:
+        return sock.send(data)
+    except socket.timeout as e:
+        message = extract_err_message(e)
+        raise WebSocketTimeoutException(message)
+    except Exception as e:
+        message = extract_err_message(e)
+        if isinstance(message, str) and "timed out" in message:
+            raise WebSocketTimeoutException(message)
+        else:
+            raise

+ 44 - 0
mt/websocket/_ssl_compat.py

@@ -0,0 +1,44 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1335  USA
+
+"""
+__all__ = ["HAVE_SSL", "ssl", "SSLError"]
+
+try:
+    import ssl
+    from ssl import SSLError
+    if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'):
+        HAVE_CONTEXT_CHECK_HOSTNAME = True
+    else:
+        HAVE_CONTEXT_CHECK_HOSTNAME = False
+        if hasattr(ssl, "match_hostname"):
+            from ssl import match_hostname
+        else:
+            from backports.ssl_match_hostname import match_hostname
+        __all__.append("match_hostname")
+    __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME")
+
+    HAVE_SSL = True
+except ImportError:
+    # dummy class of SSLError for ssl none-support environment.
+    class SSLError(Exception):
+        pass
+
+    HAVE_SSL = False

+ 163 - 0
mt/websocket/_url.py

@@ -0,0 +1,163 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA  02110-1335  USA
+
+"""
+
+import os
+import socket
+import struct
+
+from six.moves.urllib.parse import urlparse
+
+
+__all__ = ["parse_url", "get_proxy_info"]
+
+
+def parse_url(url):
+    """
+    parse url and the result is tuple of
+    (hostname, port, resource path and the flag of secure mode)
+
+    url: url string.
+    """
+    if ":" not in url:
+        raise ValueError("url is invalid")
+
+    scheme, url = url.split(":", 1)
+
+    parsed = urlparse(url, scheme="ws")
+    if parsed.hostname:
+        hostname = parsed.hostname
+    else:
+        raise ValueError("hostname is invalid")
+    port = 0
+    if parsed.port:
+        port = parsed.port
+
+    is_secure = False
+    if scheme == "ws":
+        if not port:
+            port = 80
+    elif scheme == "wss":
+        is_secure = True
+        if not port:
+            port = 443
+    else:
+        raise ValueError("scheme %s is invalid" % scheme)
+
+    if parsed.path:
+        resource = parsed.path
+    else:
+        resource = "/"
+
+    if parsed.query:
+        resource += "?" + parsed.query
+
+    return hostname, port, resource, is_secure
+
+
+DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"]
+
+
+def _is_ip_address(addr):
+    try:
+        socket.inet_aton(addr)
+    except socket.error:
+        return False
+    else:
+        return True
+
+
+def _is_subnet_address(hostname):
+    try:
+        addr, netmask = hostname.split("/")
+        return _is_ip_address(addr) and 0 <= int(netmask) < 32
+    except ValueError:
+        return False
+
+
+def _is_address_in_network(ip, net):
+    ipaddr = struct.unpack('I', socket.inet_aton(ip))[0]
+    netaddr, bits = net.split('/')
+    netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1)
+    return ipaddr & netmask == netmask
+
+
+def _is_no_proxy_host(hostname, no_proxy):
+    if not no_proxy:
+        v = os.environ.get("no_proxy", "").replace(" ", "")
+        no_proxy = v.split(",")
+    if not no_proxy:
+        no_proxy = DEFAULT_NO_PROXY_HOST
+
+    if hostname in no_proxy:
+        return True
+    elif _is_ip_address(hostname):
+        return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)])
+
+    return False
+
+
+def get_proxy_info(
+        hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None,
+        no_proxy=None, proxy_type='http'):
+    """
+    try to retrieve proxy host and port from environment
+    if not provided in options.
+    result is (proxy_host, proxy_port, proxy_auth).
+    proxy_auth is tuple of username and password
+     of proxy authentication information.
+
+    hostname: websocket server name.
+
+    is_secure:  is the connection secure? (wss)
+                looks for "https_proxy" in env
+                before falling back to "http_proxy"
+
+    options:    "http_proxy_host" - http proxy host name.
+                "http_proxy_port" - http proxy port.
+                "http_no_proxy"   - host names, which doesn't use proxy.
+                "http_proxy_auth" - http proxy auth information.
+                                    tuple of username and password.
+                                    default is None
+                "proxy_type"      - if set to "socks5" PySocks wrapper
+                                    will be used in place of a http proxy.
+                                    default is "http"
+    """
+    if _is_no_proxy_host(hostname, no_proxy):
+        return None, 0, None
+
+    if proxy_host:
+        port = proxy_port
+        auth = proxy_auth
+        return proxy_host, port, auth
+
+    env_keys = ["http_proxy"]
+    if is_secure:
+        env_keys.insert(0, "https_proxy")
+
+    for key in env_keys:
+        value = os.environ.get(key, None)
+        if value:
+            proxy = urlparse(value)
+            auth = (proxy.username, proxy.password) if proxy.username else None
+            return proxy.hostname, proxy.port, auth
+
+    return None, 0, None

+ 105 - 0
mt/websocket/_utils.py

@@ -0,0 +1,105 @@
+"""
+websocket - WebSocket client library for Python
+
+Copyright (C) 2010 Hiroki Ohtani(liris)
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor,
+    Boston, MA 02110-1335  USA
+
+"""
+import six
+
+__all__ = ["NoLock", "validate_utf8", "extract_err_message"]
+
+
+class NoLock(object):
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        pass
+
+try:
+    # If wsaccel is available we use compiled routines to validate UTF-8
+    # strings.
+    from wsaccel.utf8validator import Utf8Validator
+
+    def _validate_utf8(utfbytes):
+        return Utf8Validator().validate(utfbytes)[0]
+
+except ImportError:
+    # UTF-8 validator
+    # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/
+
+    _UTF8_ACCEPT = 0
+    _UTF8_REJECT = 12
+
+    _UTF8D = [
+        # The first part of the table maps bytes to character classes that
+        # to reduce the size of the transition table and create bitmasks.
+        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+        0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+        1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,  9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
+        7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
+        8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+        10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8,
+
+        # The second part is a transition table that maps a combination
+        # of a state of the automaton and a character class to a state.
+        0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
+        12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
+        12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
+        12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
+        12,36,12,12,12,12,12,12,12,12,12,12, ]
+
+    def _decode(state, codep, ch):
+        tp = _UTF8D[ch]
+
+        codep = (ch & 0x3f) | (codep << 6) if (
+            state != _UTF8_ACCEPT) else (0xff >> tp) & ch
+        state = _UTF8D[256 + state + tp]
+
+        return state, codep
+
+    def _validate_utf8(utfbytes):
+        state = _UTF8_ACCEPT
+        codep = 0
+        for i in utfbytes:
+            if six.PY2:
+                i = ord(i)
+            state, codep = _decode(state, codep, i)
+            if state == _UTF8_REJECT:
+                return False
+
+        return True
+
+
+def validate_utf8(utfbytes):
+    """
+    validate utf8 byte string.
+    utfbytes: utf byte string to check.
+    return value: if valid utf8 string, return true. Otherwise, return false.
+    """
+    return _validate_utf8(utfbytes)
+
+
+def extract_err_message(exception):
+    if exception.args:
+        return exception.args[0]
+    else:
+        return None

+ 18 - 14
mt/ws.py

@@ -9,10 +9,10 @@ import sys
 
 if sys.version_info[0] < 3:
 	import httplib as httplib
-	import ___websocket as websocket
+	import websocket._app as websocket
 else:
 	import http.client as httplib
-	import ___websocket as websocket
+	import websocket._app as websocket
 
 '''
 	a friendly wrapper around python-websockets that doubles as a socketio client
@@ -21,7 +21,6 @@ else:
 	_chlogh		a reference to an object that implements onchangelog(), this
   					method is called upon reception of changelogs from the asworker
 				  	we're subscribed to 
-	_dummy		true if this is a 'dummy' websocket... see note in main.py
 	subscribed  describes the current state of our subscription to our asworker
 						None:  don't know yet
 						True:  subscribed
@@ -44,11 +43,15 @@ class WebSocket :
 		assert chlogh == None or 'onchangelog' in dir(chlogh)
 		self._opened 	 = False
 		self._chlogh 	 = chlogh
-		self._dummy	 	 = (chlogh == None)
 		self.subscribed = None
 		self.connect()
 
-
+	def _start_ws(self, hskey):
+		self._ws = websocket.WebSocketApp(
+			'ws://127.0.0.1:8124/socket.io/1/websocket/' + hskey,
+			on_message = self._onmessage,
+			on_open = self._onopen)
+		self._ws.run_forever()
 
 	'''
 		connect to the socketio server
@@ -70,10 +73,11 @@ class WebSocket :
 				pass
 
 			hskey = resp.split(':')[0]
-			self._ws = websocket.WebSocket(
-				'ws://127.0.0.1:8124/socket.io/1/websocket/'+hskey,
-				onopen	 = self._onopen,
-				onmessage = self._onmessage)
+
+			# start the websocket on a different thread as it loops forever
+			thr = threading.Thread(target = self._start_ws, args = (hskey, ))
+			thr.start()
+
 		else :
 			raise Exception('websocket initialization failed :: '+str(resp.reason))
 
@@ -81,16 +85,16 @@ class WebSocket :
 
 	'''
 		close the socket '''
-	def close(self) :
+	def close(self, ws) :
 		self._ws.close()
 
 
 
 	''' 
 		parse and handle incoming message '''
-	def _onmessage(self,msg) :
-		if not self._dummy :
-			logging.debug('## msg recvd '+msg)
+	def _onmessage(self,ws, msg) :
+
+		logging.debug('## msg recvd '+msg)
 
 		msgType = msg[0]
 		if msgType == WebSocket.CONNECT :
@@ -123,7 +127,7 @@ class WebSocket :
 
 	''' 
 		mark socket connection as opened '''
-	def _onopen(self) :
+	def _onopen(self, ws) :
 		self._opened = True