1"""
2websocket - WebSocket client library for Python
3
4Copyright (C) 2010 Hiroki Ohtani(liris)
5
6    This library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Lesser General Public
8    License as published by the Free Software Foundation; either
9    version 2.1 of the License, or (at your option) any later version.
10
11    This library is distributed in the hope that it will be useful,
12    but WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    Lesser General Public License for more details.
15
16    You should have received a copy of the GNU Lesser General Public
17    License along with this library; if not, write to the Free Software
18    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20"""
21
22
23import socket
24
25try:
26    import ssl
27    from ssl import SSLError
28    HAVE_SSL = True
29except ImportError:
30    # dummy class of SSLError for ssl none-support environment.
31    class SSLError(Exception):
32        pass
33
34    HAVE_SSL = False
35
36from urlparse import urlparse
37import os
38import array
39import struct
40import uuid
41import hashlib
42import base64
43import threading
44import time
45import logging
46import traceback
47import sys
48
49"""
50websocket python client.
51=========================
52
53This version support only hybi-13.
54Please see http://tools.ietf.org/html/rfc6455 for protocol.
55"""
56
57
58# websocket supported version.
59VERSION = 13
60
61# closing frame status codes.
62STATUS_NORMAL = 1000
63STATUS_GOING_AWAY = 1001
64STATUS_PROTOCOL_ERROR = 1002
65STATUS_UNSUPPORTED_DATA_TYPE = 1003
66STATUS_STATUS_NOT_AVAILABLE = 1005
67STATUS_ABNORMAL_CLOSED = 1006
68STATUS_INVALID_PAYLOAD = 1007
69STATUS_POLICY_VIOLATION = 1008
70STATUS_MESSAGE_TOO_BIG = 1009
71STATUS_INVALID_EXTENSION = 1010
72STATUS_UNEXPECTED_CONDITION = 1011
73STATUS_TLS_HANDSHAKE_ERROR = 1015
74
75logger = logging.getLogger()
76
77
78class WebSocketException(Exception):
79    """
80    websocket exeception class.
81    """
82    pass
83
84
85class WebSocketConnectionClosedException(WebSocketException):
86    """
87    If remote host closed the connection or some network error happened,
88    this exception will be raised.
89    """
90    pass
91
92class WebSocketTimeoutException(WebSocketException):
93    """
94    WebSocketTimeoutException will be raised at socket timeout during read/write data.
95    """
96    pass
97
98default_timeout = None
99traceEnabled = False
100
101
102def enableTrace(tracable):
103    """
104    turn on/off the tracability.
105
106    tracable: boolean value. if set True, tracability is enabled.
107    """
108    global traceEnabled
109    traceEnabled = tracable
110    if tracable:
111        if not logger.handlers:
112            logger.addHandler(logging.StreamHandler())
113        logger.setLevel(logging.DEBUG)
114
115
116def setdefaulttimeout(timeout):
117    """
118    Set the global timeout setting to connect.
119
120    timeout: default socket timeout time. This value is second.
121    """
122    global default_timeout
123    default_timeout = timeout
124
125
126def getdefaulttimeout():
127    """
128    Return the global timeout setting(second) to connect.
129    """
130    return default_timeout
131
132
133def _parse_url(url):
134    """
135    parse url and the result is tuple of
136    (hostname, port, resource path and the flag of secure mode)
137
138    url: url string.
139    """
140    if ":" not in url:
141        raise ValueError("url is invalid")
142
143    scheme, url = url.split(":", 1)
144
145    parsed = urlparse(url, scheme="http")
146    if parsed.hostname:
147        hostname = parsed.hostname
148    else:
149        raise ValueError("hostname is invalid")
150    port = 0
151    if parsed.port:
152        port = parsed.port
153
154    is_secure = False
155    if scheme == "ws":
156        if not port:
157            port = 80
158    elif scheme == "wss":
159        is_secure = True
160        if not port:
161            port = 443
162    else:
163        raise ValueError("scheme %s is invalid" % scheme)
164
165    if parsed.path:
166        resource = parsed.path
167    else:
168        resource = "/"
169
170    if parsed.query:
171        resource += "?" + parsed.query
172
173    return (hostname, port, resource, is_secure)
174
175
176def create_connection(url, timeout=None, **options):
177    """
178    connect to url and return websocket object.
179
180    Connect to url and return the WebSocket object.
181    Passing optional timeout parameter will set the timeout on the socket.
182    If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
183    You can customize using 'options'.
184    If you set "header" list object, you can set your own custom header.
185
186    >>> conn = create_connection("ws://echo.websocket.org/",
187         ...     header=["User-Agent: MyProgram",
188         ...             "x-custom: header"])
189
190
191    timeout: socket timeout time. This value is integer.
192             if you set None for this value, it means "use default_timeout value"
193
194    options: current support option is only "header".
195             if you set header as dict value, the custom HTTP headers are added.
196    """
197    sockopt = options.get("sockopt", [])
198    sslopt = options.get("sslopt", {})
199    websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
200    websock.settimeout(timeout if timeout is not None else default_timeout)
201    websock.connect(url, **options)
202    return websock
203
204_MAX_INTEGER = (1 << 32) -1
205_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
206_MAX_CHAR_BYTE = (1<<8) -1
207
208# ref. Websocket gets an update, and it breaks stuff.
209# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
210
211
212def _create_sec_websocket_key():
213    uid = uuid.uuid4()
214    return base64.encodestring(uid.bytes).strip()
215
216
217_HEADERS_TO_CHECK = {
218    "upgrade": "websocket",
219    "connection": "upgrade",
220    }
221
222
223class ABNF(object):
224    """
225    ABNF frame class.
226    see http://tools.ietf.org/html/rfc5234
227    and http://tools.ietf.org/html/rfc6455#section-5.2
228    """
229
230    # operation code values.
231    OPCODE_CONT   = 0x0
232    OPCODE_TEXT   = 0x1
233    OPCODE_BINARY = 0x2
234    OPCODE_CLOSE  = 0x8
235    OPCODE_PING   = 0x9
236    OPCODE_PONG   = 0xa
237
238    # available operation code value tuple
239    OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
240                OPCODE_PING, OPCODE_PONG)
241
242    # opcode human readable string
243    OPCODE_MAP = {
244        OPCODE_CONT: "cont",
245        OPCODE_TEXT: "text",
246        OPCODE_BINARY: "binary",
247        OPCODE_CLOSE: "close",
248        OPCODE_PING: "ping",
249        OPCODE_PONG: "pong"
250        }
251
252    # data length threashold.
253    LENGTH_7  = 0x7d
254    LENGTH_16 = 1 << 16
255    LENGTH_63 = 1 << 63
256
257    def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
258                 opcode=OPCODE_TEXT, mask=1, data=""):
259        """
260        Constructor for ABNF.
261        please check RFC for arguments.
262        """
263        self.fin = fin
264        self.rsv1 = rsv1
265        self.rsv2 = rsv2
266        self.rsv3 = rsv3
267        self.opcode = opcode
268        self.mask = mask
269        self.data = data
270        self.get_mask_key = os.urandom
271
272    def __str__(self):
273        return "fin=" + str(self.fin) \
274                + " opcode=" + str(self.opcode) \
275                + " data=" + str(self.data)
276
277    @staticmethod
278    def create_frame(data, opcode):
279        """
280        create frame to send text, binary and other data.
281
282        data: data to send. This is string value(byte array).
283            if opcode is OPCODE_TEXT and this value is uniocde,
284            data value is conveted into unicode string, automatically.
285
286        opcode: operation code. please see OPCODE_XXX.
287        """
288        if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
289            data = data.encode("utf-8")
290        # mask must be set if send data from client
291        return ABNF(1, 0, 0, 0, opcode, 1, data)
292
293    def format(self):
294        """
295        format this object to string(byte array) to send data to server.
296        """
297        if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
298            raise ValueError("not 0 or 1")
299        if self.opcode not in ABNF.OPCODES:
300            raise ValueError("Invalid OPCODE")
301        length = len(self.data)
302        if length >= ABNF.LENGTH_63:
303            raise ValueError("data is too long")
304
305        frame_header = chr(self.fin << 7
306                           | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
307                           | self.opcode)
308        if length < ABNF.LENGTH_7:
309            frame_header += chr(self.mask << 7 | length)
310        elif length < ABNF.LENGTH_16:
311            frame_header += chr(self.mask << 7 | 0x7e)
312            frame_header += struct.pack("!H", length)
313        else:
314            frame_header += chr(self.mask << 7 | 0x7f)
315            frame_header += struct.pack("!Q", length)
316
317        if not self.mask:
318            return frame_header + self.data
319        else:
320            mask_key = self.get_mask_key(4)
321            return frame_header + self._get_masked(mask_key)
322
323    def _get_masked(self, mask_key):
324        s = ABNF.mask(mask_key, self.data)
325        return mask_key + "".join(s)
326
327    @staticmethod
328    def mask(mask_key, data):
329        """
330        mask or unmask data. Just do xor for each byte
331
332        mask_key: 4 byte string(byte).
333
334        data: data to mask/unmask.
335        """
336        _m = array.array("B", mask_key)
337        _d = array.array("B", data)
338        for i in xrange(len(_d)):
339            _d[i] ^= _m[i % 4]
340        return _d.tostring()
341
342
343class WebSocket(object):
344    """
345    Low level WebSocket interface.
346    This class is based on
347      The WebSocket protocol draft-hixie-thewebsocketprotocol-76
348      http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
349
350    We can connect to the websocket server and send/recieve data.
351    The following example is a echo client.
352
353    >>> import websocket
354    >>> ws = websocket.WebSocket()
355    >>> ws.connect("ws://echo.websocket.org")
356    >>> ws.send("Hello, Server")
357    >>> ws.recv()
358    'Hello, Server'
359    >>> ws.close()
360
361    get_mask_key: a callable to produce new mask keys, see the set_mask_key
362      function's docstring for more details
363    sockopt: values for socket.setsockopt.
364        sockopt must be tuple and each element is argument of sock.setscokopt.
365    sslopt: dict object for ssl socket option.
366    """
367
368    def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
369        """
370        Initalize WebSocket object.
371        """
372        if sockopt is None:
373            sockopt = []
374        if sslopt is None:
375            sslopt = {}
376        self.connected = False
377        self.sock = socket.socket()
378        for opts in sockopt:
379            self.sock.setsockopt(*opts)
380        self.sslopt = sslopt
381        self.get_mask_key = get_mask_key
382        # Buffers over the packets from the layer beneath until desired amount
383        # bytes of bytes are received.
384        self._recv_buffer = []
385        # These buffer over the build-up of a single frame.
386        self._frame_header = None
387        self._frame_length = None
388        self._frame_mask = None
389        self._cont_data = None
390
391    def fileno(self):
392        return self.sock.fileno()
393
394    def set_mask_key(self, func):
395        """
396        set function to create musk key. You can custumize mask key generator.
397        Mainly, this is for testing purpose.
398
399        func: callable object. the fuct must 1 argument as integer.
400              The argument means length of mask key.
401              This func must be return string(byte array),
402              which length is argument specified.
403        """
404        self.get_mask_key = func
405
406    def gettimeout(self):
407        """
408        Get the websocket timeout(second).
409        """
410        return self.sock.gettimeout()
411
412    def settimeout(self, timeout):
413        """
414        Set the timeout to the websocket.
415
416        timeout: timeout time(second).
417        """
418        self.sock.settimeout(timeout)
419
420    timeout = property(gettimeout, settimeout)
421
422    def connect(self, url, **options):
423        """
424        Connect to url. url is websocket url scheme. ie. ws://host:port/resource
425        You can customize using 'options'.
426        If you set "header" dict object, you can set your own custom header.
427
428        >>> ws = WebSocket()
429        >>> ws.connect("ws://echo.websocket.org/",
430                ...     header={"User-Agent: MyProgram",
431                ...             "x-custom: header"})
432
433        timeout: socket timeout time. This value is integer.
434                 if you set None for this value,
435                 it means "use default_timeout value"
436
437        options: current support option is only "header".
438                 if you set header as dict value,
439                 the custom HTTP headers are added.
440
441        """
442        hostname, port, resource, is_secure = _parse_url(url)
443        # TODO: we need to support proxy
444        self.sock.connect((hostname, port))
445        if is_secure:
446            if HAVE_SSL:
447                if self.sslopt is None:
448                    sslopt = {}
449                else:
450                    sslopt = self.sslopt
451                self.sock = ssl.wrap_socket(self.sock, **sslopt)
452            else:
453                raise WebSocketException("SSL not available.")
454
455        self._handshake(hostname, port, resource, **options)
456
457    def _handshake(self, host, port, resource, **options):
458        sock = self.sock
459        headers = []
460        headers.append("GET %s HTTP/1.1" % resource)
461        headers.append("Upgrade: websocket")
462        headers.append("Connection: Upgrade")
463        if port == 80:
464            hostport = host
465        else:
466            hostport = "%s:%d" % (host, port)
467        headers.append("Host: %s" % hostport)
468
469        if "origin" in options:
470            headers.append("Origin: %s" % options["origin"])
471        else:
472            headers.append("Origin: http://%s" % hostport)
473
474        key = _create_sec_websocket_key()
475        headers.append("Sec-WebSocket-Key: %s" % key)
476        headers.append("Sec-WebSocket-Version: %s" % VERSION)
477        if "header" in options:
478            headers.extend(options["header"])
479
480        headers.append("")
481        headers.append("")
482
483        header_str = "\r\n".join(headers)
484        self._send(header_str)
485        if traceEnabled:
486            logger.debug("--- request header ---")
487            logger.debug(header_str)
488            logger.debug("-----------------------")
489
490        status, resp_headers = self._read_headers()
491        if status != 101:
492            self.close()
493            raise WebSocketException("Handshake Status %d" % status)
494
495        success = self._validate_header(resp_headers, key)
496        if not success:
497            self.close()
498            raise WebSocketException("Invalid WebSocket Header")
499
500        self.connected = True
501
502    def _validate_header(self, headers, key):
503        for k, v in _HEADERS_TO_CHECK.iteritems():
504            r = headers.get(k, None)
505            if not r:
506                return False
507            r = r.lower()
508            if v != r:
509                return False
510
511        result = headers.get("sec-websocket-accept", None)
512        if not result:
513            return False
514        result = result.lower()
515
516        value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
517        hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
518        return hashed == result
519
520    def _read_headers(self):
521        status = None
522        headers = {}
523        if traceEnabled:
524            logger.debug("--- response header ---")
525
526        while True:
527            line = self._recv_line()
528            if line == "\r\n":
529                break
530            line = line.strip()
531            if traceEnabled:
532                logger.debug(line)
533            if not status:
534                status_info = line.split(" ", 2)
535                status = int(status_info[1])
536            else:
537                kv = line.split(":", 1)
538                if len(kv) == 2:
539                    key, value = kv
540                    headers[key.lower()] = value.strip().lower()
541                else:
542                    raise WebSocketException("Invalid header")
543
544        if traceEnabled:
545            logger.debug("-----------------------")
546
547        return status, headers
548
549    def send(self, payload, opcode=ABNF.OPCODE_TEXT):
550        """
551        Send the data as string.
552
553        payload: Payload must be utf-8 string or unicoce,
554                  if the opcode is OPCODE_TEXT.
555                  Otherwise, it must be string(byte array)
556
557        opcode: operation code to send. Please see OPCODE_XXX.
558        """
559        frame = ABNF.create_frame(payload, opcode)
560        if self.get_mask_key:
561            frame.get_mask_key = self.get_mask_key
562        data = frame.format()
563        length = len(data)
564        if traceEnabled:
565            logger.debug("send: " + repr(data))
566        while data:
567            l = self._send(data)
568            data = data[l:]
569        return length
570
571    def send_binary(self, payload):
572        return self.send(payload, ABNF.OPCODE_BINARY)
573
574    def ping(self, payload=""):
575        """
576        send ping data.
577
578        payload: data payload to send server.
579        """
580        self.send(payload, ABNF.OPCODE_PING)
581
582    def pong(self, payload):
583        """
584        send pong data.
585
586        payload: data payload to send server.
587        """
588        self.send(payload, ABNF.OPCODE_PONG)
589
590    def recv(self):
591        """
592        Receive string data(byte array) from the server.
593
594        return value: string(byte array) value.
595        """
596        opcode, data = self.recv_data()
597        return data
598
599    def recv_data(self):
600        """
601        Recieve data with operation code.
602
603        return  value: tuple of operation code and string(byte array) value.
604        """
605        while True:
606            frame = self.recv_frame()
607            if not frame:
608                # handle error:
609                # 'NoneType' object has no attribute 'opcode'
610                raise WebSocketException("Not a valid frame %s" % frame)
611            elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
612                if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
613                    raise WebSocketException("Illegal frame")
614                if self._cont_data:
615                    self._cont_data[1] += frame.data
616                else:
617                    self._cont_data = [frame.opcode, frame.data]
618
619                if frame.fin:
620                    data = self._cont_data
621                    self._cont_data = None
622                    return data
623            elif frame.opcode == ABNF.OPCODE_CLOSE:
624                self.send_close()
625                return (frame.opcode, None)
626            elif frame.opcode == ABNF.OPCODE_PING:
627                self.pong(frame.data)
628
629    def recv_frame(self):
630        """
631        recieve data as frame from server.
632
633        return value: ABNF frame object.
634        """
635        # Header
636        if self._frame_header is None:
637            self._frame_header = self._recv_strict(2)
638        b1 = ord(self._frame_header[0])
639        fin = b1 >> 7 & 1
640        rsv1 = b1 >> 6 & 1
641        rsv2 = b1 >> 5 & 1
642        rsv3 = b1 >> 4 & 1
643        opcode = b1 & 0xf
644        b2 = ord(self._frame_header[1])
645        has_mask = b2 >> 7 & 1
646        # Frame length
647        if self._frame_length is None:
648            length_bits = b2 & 0x7f
649            if length_bits == 0x7e:
650                length_data = self._recv_strict(2)
651                self._frame_length = struct.unpack("!H", length_data)[0]
652            elif length_bits == 0x7f:
653                length_data = self._recv_strict(8)
654                self._frame_length = struct.unpack("!Q", length_data)[0]
655            else:
656                self._frame_length = length_bits
657        # Mask
658        if self._frame_mask is None:
659            self._frame_mask = self._recv_strict(4) if has_mask else ""
660        # Payload
661        payload = self._recv_strict(self._frame_length)
662        if has_mask:
663            payload = ABNF.mask(self._frame_mask, payload)
664        # Reset for next frame
665        self._frame_header = None
666        self._frame_length = None
667        self._frame_mask = None
668        return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
669
670
671    def send_close(self, status=STATUS_NORMAL, reason=""):
672        """
673        send close data to the server.
674
675        status: status code to send. see STATUS_XXX.
676
677        reason: the reason to close. This must be string.
678        """
679        if status < 0 or status >= ABNF.LENGTH_16:
680            raise ValueError("code is invalid range")
681        self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
682
683    def close(self, status=STATUS_NORMAL, reason=""):
684        """
685        Close Websocket object
686
687        status: status code to send. see STATUS_XXX.
688
689        reason: the reason to close. This must be string.
690        """
691        if self.connected:
692            if status < 0 or status >= ABNF.LENGTH_16:
693                raise ValueError("code is invalid range")
694
695            try:
696                self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
697                timeout = self.sock.gettimeout()
698                self.sock.settimeout(3)
699                try:
700                    frame = self.recv_frame()
701                    if logger.isEnabledFor(logging.ERROR):
702                        recv_status = struct.unpack("!H", frame.data)[0]
703                        if recv_status != STATUS_NORMAL:
704                            logger.error("close status: " + repr(recv_status))
705                except:
706                    pass
707                self.sock.settimeout(timeout)
708                self.sock.shutdown(socket.SHUT_RDWR)
709            except:
710                pass
711        self._closeInternal()
712
713    def _closeInternal(self):
714        self.connected = False
715        self.sock.close()
716
717    def _send(self, data):
718        try:
719            return self.sock.send(data)
720        except socket.timeout as e:
721            raise WebSocketTimeoutException(e.message)
722        except Exception as e:
723            if "timed out" in e.message:
724                raise WebSocketTimeoutException(e.message)
725            else:
726                raise e
727
728    def _recv(self, bufsize):
729        try:
730            bytes = self.sock.recv(bufsize)
731        except socket.timeout as e:
732            raise WebSocketTimeoutException(e.message)
733        except SSLError as e:
734            if e.message == "The read operation timed out":
735                raise WebSocketTimeoutException(e.message)
736            else:
737                raise
738        if not bytes:
739            raise WebSocketConnectionClosedException()
740        return bytes
741
742
743    def _recv_strict(self, bufsize):
744        shortage = bufsize - sum(len(x) for x in self._recv_buffer)
745        while shortage > 0:
746            bytes = self._recv(shortage)
747            self._recv_buffer.append(bytes)
748            shortage -= len(bytes)
749        unified = "".join(self._recv_buffer)
750        if shortage == 0:
751            self._recv_buffer = []
752            return unified
753        else:
754            self._recv_buffer = [unified[bufsize:]]
755            return unified[:bufsize]
756
757
758    def _recv_line(self):
759        line = []
760        while True:
761            c = self._recv(1)
762            line.append(c)
763            if c == "\n":
764                break
765        return "".join(line)
766
767
768class WebSocketApp(object):
769    """
770    Higher level of APIs are provided.
771    The interface is like JavaScript WebSocket object.
772    """
773    def __init__(self, url, header=[],
774                 on_open=None, on_message=None, on_error=None,
775                 on_close=None, keep_running=True, get_mask_key=None):
776        """
777        url: websocket url.
778        header: custom header for websocket handshake.
779        on_open: callable object which is called at opening websocket.
780          this function has one argument. The arugment is this class object.
781        on_message: callbale object which is called when recieved data.
782         on_message has 2 arguments.
783         The 1st arugment is this class object.
784         The passing 2nd arugment is utf-8 string which we get from the server.
785       on_error: callable object which is called when we get error.
786         on_error has 2 arguments.
787         The 1st arugment is this class object.
788         The passing 2nd arugment is exception object.
789       on_close: callable object which is called when closed the connection.
790         this function has one argument. The arugment is this class object.
791       keep_running: a boolean flag indicating whether the app's main loop should
792         keep running, defaults to True
793       get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
794         docstring for more information
795        """
796        self.url = url
797        self.header = header
798        self.on_open = on_open
799        self.on_message = on_message
800        self.on_error = on_error
801        self.on_close = on_close
802        self.keep_running = keep_running
803        self.get_mask_key = get_mask_key
804        self.sock = None
805
806    def send(self, data, opcode=ABNF.OPCODE_TEXT):
807        """
808        send message.
809        data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
810        opcode: operation code of data. default is OPCODE_TEXT.
811        """
812        if self.sock.send(data, opcode) == 0:
813            raise WebSocketConnectionClosedException()
814
815    def close(self):
816        """
817        close websocket connection.
818        """
819        self.keep_running = False
820        self.sock.close()
821
822    def _send_ping(self, interval):
823        while True:
824            for i in range(interval):
825                time.sleep(1)
826                if not self.keep_running:
827                    return
828            self.sock.ping()
829
830    def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
831        """
832        run event loop for WebSocket framework.
833        This loop is infinite loop and is alive during websocket is available.
834        sockopt: values for socket.setsockopt.
835            sockopt must be tuple and each element is argument of sock.setscokopt.
836        sslopt: ssl socket optional dict.
837        ping_interval: automatically send "ping" command every specified period(second)
838            if set to 0, not send automatically.
839        """
840        if sockopt is None:
841            sockopt = []
842        if sslopt is None:
843            sslopt = {}
844        if self.sock:
845            raise WebSocketException("socket is already opened")
846        thread = None
847
848        try:
849            self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt)
850            self.sock.settimeout(default_timeout)
851            self.sock.connect(self.url, header=self.header)
852            self._callback(self.on_open)
853
854            if ping_interval:
855                thread = threading.Thread(target=self._send_ping, args=(ping_interval,))
856                thread.setDaemon(True)
857                thread.start()
858
859            while self.keep_running:
860                data = self.sock.recv()
861                if data is None:
862                    break
863                self._callback(self.on_message, data)
864        except Exception, e:
865            self._callback(self.on_error, e)
866        finally:
867            if thread:
868                self.keep_running = False
869            self.sock.close()
870            self._callback(self.on_close)
871            self.sock = None
872
873    def _callback(self, callback, *args):
874        if callback:
875            try:
876                callback(self, *args)
877            except Exception, e:
878                logger.error(e)
879                if logger.isEnabledFor(logging.DEBUG):
880                    _, _, tb = sys.exc_info()
881                    traceback.print_tb(tb)
882
883
884if __name__ == "__main__":
885    enableTrace(True)
886    ws = create_connection("ws://echo.websocket.org/")
887    print("Sending 'Hello, World'...")
888    ws.send("Hello, World")
889    print("Sent")
890    print("Receiving...")
891    result = ws.recv()
892    print("Received '%s'" % result)
893    ws.close()
894