~ubuntu-branches/ubuntu/trusty/websockify/trusty-updates

« back to all changes in this revision

Viewing changes to websockify/websocket.py

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2013-02-23 01:22:51 UTC
  • Revision ID: package-import@ubuntu.com-20130223012251-3qkk1n1p93kb3j87
Tags: upstream-0.3.0+dfsg1
ImportĀ upstreamĀ versionĀ 0.3.0+dfsg1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
 
 
3
'''
 
4
Python WebSocket library with support for "wss://" encryption.
 
5
Copyright 2011 Joel Martin
 
6
Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
 
7
 
 
8
Supports following protocol versions:
 
9
    - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
 
10
    - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
 
11
    - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
 
12
 
 
13
You can make a cert/key with openssl using:
 
14
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
 
15
as taken from http://docs.python.org/dev/library/ssl.html#certificates
 
16
 
 
17
'''
 
18
 
 
19
import os, sys, time, errno, signal, socket, traceback, select
 
20
import array, struct
 
21
from base64 import b64encode, b64decode
 
22
 
 
23
# Imports that vary by python version
 
24
 
 
25
# python 3.0 differences
 
26
if sys.hexversion > 0x3000000:
 
27
    b2s = lambda buf: buf.decode('latin_1')
 
28
    s2b = lambda s: s.encode('latin_1')
 
29
    s2a = lambda s: s
 
30
else:
 
31
    b2s = lambda buf: buf  # No-op
 
32
    s2b = lambda s: s      # No-op
 
33
    s2a = lambda s: [ord(c) for c in s]
 
34
try:    from io import StringIO
 
35
except: from cStringIO import StringIO
 
36
try:    from http.server import SimpleHTTPRequestHandler
 
37
except: from SimpleHTTPServer import SimpleHTTPRequestHandler
 
38
 
 
39
# python 2.6 differences
 
40
try:    from hashlib import md5, sha1
 
41
except: from md5 import md5; from sha import sha as sha1
 
42
 
 
43
# python 2.5 differences
 
44
try:
 
45
    from struct import pack, unpack_from
 
46
except:
 
47
    from struct import pack
 
48
    def unpack_from(fmt, buf, offset=0):
 
49
        slice = buffer(buf, offset, struct.calcsize(fmt))
 
50
        return struct.unpack(fmt, slice)
 
51
 
 
52
# Degraded functionality if these imports are missing
 
53
for mod, sup in [('numpy', 'HyBi protocol'), ('ssl', 'TLS/SSL/wss'),
 
54
        ('multiprocessing', 'Multi-Processing'),
 
55
        ('resource', 'daemonizing')]:
 
56
    try:
 
57
        globals()[mod] = __import__(mod)
 
58
    except ImportError:
 
59
        globals()[mod] = None
 
60
        print("WARNING: no '%s' module, %s is slower or disabled" % (
 
61
            mod, sup))
 
62
if multiprocessing and sys.platform == 'win32':
 
63
    # make sockets pickle-able/inheritable
 
64
    import multiprocessing.reduction
 
65
 
 
66
 
 
67
class WebSocketServer(object):
 
68
    """
 
69
    WebSockets server class.
 
70
    Must be sub-classed with new_client method definition.
 
71
    """
 
72
 
 
73
    buffer_size = 65536
 
74
 
 
75
 
 
76
    server_handshake_hixie = """HTTP/1.1 101 Web Socket Protocol Handshake\r
 
77
Upgrade: WebSocket\r
 
78
Connection: Upgrade\r
 
79
%sWebSocket-Origin: %s\r
 
80
%sWebSocket-Location: %s://%s%s\r
 
81
"""
 
82
 
 
83
    server_handshake_hybi = """HTTP/1.1 101 Switching Protocols\r
 
84
Upgrade: websocket\r
 
85
Connection: Upgrade\r
 
86
Sec-WebSocket-Accept: %s\r
 
87
"""
 
88
 
 
89
    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
 
90
 
 
91
    policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
 
92
 
 
93
    # An exception before the WebSocket connection was established
 
94
    class EClose(Exception):
 
95
        pass
 
96
 
 
97
    # An exception while the WebSocket client was connected
 
98
    class CClose(Exception):
 
99
        pass
 
100
 
 
101
    def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False,
 
102
            verbose=False, cert='', key='', ssl_only=None,
 
103
            daemon=False, record='', web='',
 
104
            run_once=False, timeout=0, idle_timeout=0):
 
105
 
 
106
        # settings
 
107
        self.verbose        = verbose
 
108
        self.listen_host    = listen_host
 
109
        self.listen_port    = listen_port
 
110
        self.prefer_ipv6    = source_is_ipv6
 
111
        self.ssl_only       = ssl_only
 
112
        self.daemon         = daemon
 
113
        self.run_once       = run_once
 
114
        self.timeout        = timeout
 
115
        self.idle_timeout   = idle_timeout
 
116
        
 
117
        self.launch_time    = time.time()
 
118
        self.ws_connection  = False
 
119
        self.handler_id     = 1
 
120
 
 
121
        # Make paths settings absolute
 
122
        self.cert = os.path.abspath(cert)
 
123
        self.key = self.web = self.record = ''
 
124
        if key:
 
125
            self.key = os.path.abspath(key)
 
126
        if web:
 
127
            self.web = os.path.abspath(web)
 
128
        if record:
 
129
            self.record = os.path.abspath(record)
 
130
 
 
131
        if self.web:
 
132
            os.chdir(self.web)
 
133
 
 
134
        # Sanity checks
 
135
        if not ssl and self.ssl_only:
 
136
            raise Exception("No 'ssl' module and SSL-only specified")
 
137
        if self.daemon and not resource:
 
138
            raise Exception("Module 'resource' required to daemonize")
 
139
 
 
140
        # Show configuration
 
141
        print("WebSocket server settings:")
 
142
        print("  - Listen on %s:%s" % (
 
143
                self.listen_host, self.listen_port))
 
144
        print("  - Flash security policy server")
 
145
        if self.web:
 
146
            print("  - Web server. Web root: %s" % self.web)
 
147
        if ssl:
 
148
            if os.path.exists(self.cert):
 
149
                print("  - SSL/TLS support")
 
150
                if self.ssl_only:
 
151
                    print("  - Deny non-SSL/TLS connections")
 
152
            else:
 
153
                print("  - No SSL/TLS support (no cert file)")
 
154
        else:
 
155
            print("  - No SSL/TLS support (no 'ssl' module)")
 
156
        if self.daemon:
 
157
            print("  - Backgrounding (daemon)")
 
158
        if self.record:
 
159
            print("  - Recording to '%s.*'" % self.record)
 
160
 
 
161
    #
 
162
    # WebSocketServer static methods
 
163
    #
 
164
 
 
165
    @staticmethod
 
166
    def socket(host, port=None, connect=False, prefer_ipv6=False, unix_socket=None, use_ssl=False):
 
167
        """ Resolve a host (and optional port) to an IPv4 or IPv6
 
168
        address. Create a socket. Bind to it if listen is set,
 
169
        otherwise connect to it. Return the socket.
 
170
        """
 
171
        flags = 0
 
172
        if host == '':
 
173
            host = None
 
174
        if connect and not (port or unix_socket):
 
175
            raise Exception("Connect mode requires a port")
 
176
        if use_ssl and not ssl:
 
177
            raise Exception("SSL socket requested but Python SSL module not loaded.");
 
178
        if not connect and use_ssl:
 
179
            raise Exception("SSL only supported in connect mode (for now)")
 
180
        if not connect:
 
181
            flags = flags | socket.AI_PASSIVE
 
182
            
 
183
        if not unix_socket:
 
184
            addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
 
185
                    socket.IPPROTO_TCP, flags)
 
186
            if not addrs:
 
187
                raise Exception("Could not resolve host '%s'" % host)
 
188
            addrs.sort(key=lambda x: x[0])
 
189
            if prefer_ipv6:
 
190
                addrs.reverse()
 
191
            sock = socket.socket(addrs[0][0], addrs[0][1])
 
192
            if connect:
 
193
                sock.connect(addrs[0][4])
 
194
                if use_ssl:
 
195
                    sock = ssl.wrap_socket(sock)
 
196
            else:
 
197
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
198
                sock.bind(addrs[0][4])
 
199
                sock.listen(100)
 
200
        else:    
 
201
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
 
202
            sock.connect(unix_socket)
 
203
 
 
204
        return sock
 
205
 
 
206
    @staticmethod
 
207
    def daemonize(keepfd=None, chdir='/'):
 
208
        os.umask(0)
 
209
        if chdir:
 
210
            os.chdir(chdir)
 
211
        else:
 
212
            os.chdir('/')
 
213
        os.setgid(os.getgid())  # relinquish elevations
 
214
        os.setuid(os.getuid())  # relinquish elevations
 
215
 
 
216
        # Double fork to daemonize
 
217
        if os.fork() > 0: os._exit(0)  # Parent exits
 
218
        os.setsid()                    # Obtain new process group
 
219
        if os.fork() > 0: os._exit(0)  # Parent exits
 
220
 
 
221
        # Signal handling
 
222
        def terminate(a,b): os._exit(0)
 
223
        signal.signal(signal.SIGTERM, terminate)
 
224
        signal.signal(signal.SIGINT, signal.SIG_IGN)
 
225
 
 
226
        # Close open files
 
227
        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
 
228
        if maxfd == resource.RLIM_INFINITY: maxfd = 256
 
229
        for fd in reversed(range(maxfd)):
 
230
            try:
 
231
                if fd != keepfd:
 
232
                    os.close(fd)
 
233
            except OSError:
 
234
                _, exc, _ = sys.exc_info()
 
235
                if exc.errno != errno.EBADF: raise
 
236
 
 
237
        # Redirect I/O to /dev/null
 
238
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno())
 
239
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno())
 
240
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno())
 
241
 
 
242
    @staticmethod
 
243
    def unmask(buf, hlen, plen):
 
244
        pstart = hlen + 4
 
245
        pend = pstart + plen
 
246
        if numpy:
 
247
            b = c = s2b('')
 
248
            if plen >= 4:
 
249
                mask = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'),
 
250
                        offset=hlen, count=1)
 
251
                data = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'),
 
252
                        offset=pstart, count=int(plen / 4))
 
253
                #b = numpy.bitwise_xor(data, mask).data
 
254
                b = numpy.bitwise_xor(data, mask).tostring()
 
255
 
 
256
            if plen % 4:
 
257
                #print("Partial unmask")
 
258
                mask = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
 
259
                        offset=hlen, count=(plen % 4))
 
260
                data = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
 
261
                        offset=pend - (plen % 4),
 
262
                        count=(plen % 4))
 
263
                c = numpy.bitwise_xor(data, mask).tostring()
 
264
            return b + c
 
265
        else:
 
266
            # Slower fallback
 
267
            mask = buf[hlen:hlen+4]
 
268
            data = array.array('B')
 
269
            mask = s2a(mask)
 
270
            data.fromstring(buf[pstart:pend])
 
271
            for i in range(len(data)):
 
272
                data[i] ^= mask[i % 4]
 
273
            return data.tostring()
 
274
 
 
275
    @staticmethod
 
276
    def encode_hybi(buf, opcode, base64=False):
 
277
        """ Encode a HyBi style WebSocket frame.
 
278
        Optional opcode:
 
279
            0x0 - continuation
 
280
            0x1 - text frame (base64 encode buf)
 
281
            0x2 - binary frame (use raw buf)
 
282
            0x8 - connection close
 
283
            0x9 - ping
 
284
            0xA - pong
 
285
        """
 
286
        if base64:
 
287
            buf = b64encode(buf)
 
288
 
 
289
        b1 = 0x80 | (opcode & 0x0f) # FIN + opcode
 
290
        payload_len = len(buf)
 
291
        if payload_len <= 125:
 
292
            header = pack('>BB', b1, payload_len)
 
293
        elif payload_len > 125 and payload_len < 65536:
 
294
            header = pack('>BBH', b1, 126, payload_len)
 
295
        elif payload_len >= 65536:
 
296
            header = pack('>BBQ', b1, 127, payload_len)
 
297
 
 
298
        #print("Encoded: %s" % repr(header + buf))
 
299
 
 
300
        return header + buf, len(header), 0
 
301
 
 
302
    @staticmethod
 
303
    def decode_hybi(buf, base64=False):
 
304
        """ Decode HyBi style WebSocket packets.
 
305
        Returns:
 
306
            {'fin'          : 0_or_1,
 
307
             'opcode'       : number,
 
308
             'masked'       : boolean,
 
309
             'hlen'         : header_bytes_number,
 
310
             'length'       : payload_bytes_number,
 
311
             'payload'      : decoded_buffer,
 
312
             'left'         : bytes_left_number,
 
313
             'close_code'   : number,
 
314
             'close_reason' : string}
 
315
        """
 
316
 
 
317
        f = {'fin'          : 0,
 
318
             'opcode'       : 0,
 
319
             'masked'       : False,
 
320
             'hlen'         : 2,
 
321
             'length'       : 0,
 
322
             'payload'      : None,
 
323
             'left'         : 0,
 
324
             'close_code'   : 1000,
 
325
             'close_reason' : ''}
 
326
 
 
327
        blen = len(buf)
 
328
        f['left'] = blen
 
329
 
 
330
        if blen < f['hlen']:
 
331
            return f # Incomplete frame header
 
332
 
 
333
        b1, b2 = unpack_from(">BB", buf)
 
334
        f['opcode'] = b1 & 0x0f
 
335
        f['fin'] = (b1 & 0x80) >> 7
 
336
        f['masked'] = (b2 & 0x80) >> 7
 
337
 
 
338
        f['length'] = b2 & 0x7f
 
339
 
 
340
        if f['length'] == 126:
 
341
            f['hlen'] = 4
 
342
            if blen < f['hlen']:
 
343
                return f # Incomplete frame header
 
344
            (f['length'],) = unpack_from('>xxH', buf)
 
345
        elif f['length'] == 127:
 
346
            f['hlen'] = 10
 
347
            if blen < f['hlen']:
 
348
                return f # Incomplete frame header
 
349
            (f['length'],) = unpack_from('>xxQ', buf)
 
350
 
 
351
        full_len = f['hlen'] + f['masked'] * 4 + f['length']
 
352
 
 
353
        if blen < full_len: # Incomplete frame
 
354
            return f # Incomplete frame header
 
355
 
 
356
        # Number of bytes that are part of the next frame(s)
 
357
        f['left'] = blen - full_len
 
358
 
 
359
        # Process 1 frame
 
360
        if f['masked']:
 
361
            # unmask payload
 
362
            f['payload'] = WebSocketServer.unmask(buf, f['hlen'],
 
363
                                                  f['length'])
 
364
        else:
 
365
            print("Unmasked frame: %s" % repr(buf))
 
366
            f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len]
 
367
 
 
368
        if base64 and f['opcode'] in [1, 2]:
 
369
            try:
 
370
                f['payload'] = b64decode(f['payload'])
 
371
            except:
 
372
                print("Exception while b64decoding buffer: %s" %
 
373
                        repr(buf))
 
374
                raise
 
375
 
 
376
        if f['opcode'] == 0x08:
 
377
            if f['length'] >= 2:
 
378
                f['close_code'] = unpack_from(">H", f['payload'])[0]
 
379
            if f['length'] > 3:
 
380
                f['close_reason'] = f['payload'][2:]
 
381
 
 
382
        return f
 
383
 
 
384
    @staticmethod
 
385
    def encode_hixie(buf):
 
386
        return s2b("\x00" + b2s(b64encode(buf)) + "\xff"), 1, 1
 
387
 
 
388
    @staticmethod
 
389
    def decode_hixie(buf):
 
390
        end = buf.find(s2b('\xff'))
 
391
        return {'payload': b64decode(buf[1:end]),
 
392
                'hlen': 1,
 
393
                'masked': False,
 
394
                'length': end - 1,
 
395
                'left': len(buf) - (end + 1)}
 
396
 
 
397
 
 
398
    @staticmethod
 
399
    def gen_md5(keys):
 
400
        """ Generate hash value for WebSockets hixie-76. """
 
401
        key1 = keys['Sec-WebSocket-Key1']
 
402
        key2 = keys['Sec-WebSocket-Key2']
 
403
        key3 = keys['key3']
 
404
        spaces1 = key1.count(" ")
 
405
        spaces2 = key2.count(" ")
 
406
        num1 = int("".join([c for c in key1 if c.isdigit()])) / spaces1
 
407
        num2 = int("".join([c for c in key2 if c.isdigit()])) / spaces2
 
408
 
 
409
        return b2s(md5(pack('>II8s',
 
410
            int(num1), int(num2), key3)).digest())
 
411
 
 
412
    #
 
413
    # WebSocketServer logging/output functions
 
414
    #
 
415
 
 
416
    def traffic(self, token="."):
 
417
        """ Show traffic flow in verbose mode. """
 
418
        if self.verbose and not self.daemon:
 
419
            sys.stdout.write(token)
 
420
            sys.stdout.flush()
 
421
 
 
422
    def msg(self, msg):
 
423
        """ Output message with handler_id prefix. """
 
424
        if not self.daemon:
 
425
            print("% 3d: %s" % (self.handler_id, msg))
 
426
 
 
427
    def vmsg(self, msg):
 
428
        """ Same as msg() but only if verbose. """
 
429
        if self.verbose:
 
430
            self.msg(msg)
 
431
 
 
432
    #
 
433
    # Main WebSocketServer methods
 
434
    #
 
435
    def send_frames(self, bufs=None):
 
436
        """ Encode and send WebSocket frames. Any frames already
 
437
        queued will be sent first. If buf is not set then only queued
 
438
        frames will be sent. Returns the number of pending frames that
 
439
        could not be fully sent. If returned pending frames is greater
 
440
        than 0, then the caller should call again when the socket is
 
441
        ready. """
 
442
 
 
443
        tdelta = int(time.time()*1000) - self.start_time
 
444
 
 
445
        if bufs:
 
446
            for buf in bufs:
 
447
                if self.version.startswith("hybi"):
 
448
                    if self.base64:
 
449
                        encbuf, lenhead, lentail = self.encode_hybi(
 
450
                                buf, opcode=1, base64=True)
 
451
                    else:
 
452
                        encbuf, lenhead, lentail = self.encode_hybi(
 
453
                                buf, opcode=2, base64=False)
 
454
 
 
455
                else:
 
456
                    encbuf, lenhead, lentail = self.encode_hixie(buf)
 
457
 
 
458
                if self.rec:
 
459
                    self.rec.write("%s,\n" %
 
460
                            repr("{%s{" % tdelta
 
461
                                + encbuf[lenhead:len(encbuf)-lentail]))
 
462
 
 
463
                self.send_parts.append(encbuf)
 
464
 
 
465
        while self.send_parts:
 
466
            # Send pending frames
 
467
            buf = self.send_parts.pop(0)
 
468
            sent = self.client.send(buf)
 
469
 
 
470
            if sent == len(buf):
 
471
                self.traffic("<")
 
472
            else:
 
473
                self.traffic("<.")
 
474
                self.send_parts.insert(0, buf[sent:])
 
475
                break
 
476
 
 
477
        return len(self.send_parts)
 
478
 
 
479
    def recv_frames(self):
 
480
        """ Receive and decode WebSocket frames.
 
481
 
 
482
        Returns:
 
483
            (bufs_list, closed_string)
 
484
        """
 
485
 
 
486
        closed = False
 
487
        bufs = []
 
488
        tdelta = int(time.time()*1000) - self.start_time
 
489
 
 
490
        buf = self.client.recv(self.buffer_size)
 
491
        if len(buf) == 0:
 
492
            closed = {'code': 1000, 'reason': "Client closed abruptly"}
 
493
            return bufs, closed
 
494
 
 
495
        if self.recv_part:
 
496
            # Add partially received frames to current read buffer
 
497
            buf = self.recv_part + buf
 
498
            self.recv_part = None
 
499
 
 
500
        while buf:
 
501
            if self.version.startswith("hybi"):
 
502
 
 
503
                frame = self.decode_hybi(buf, base64=self.base64)
 
504
                #print("Received buf: %s, frame: %s" % (repr(buf), frame))
 
505
 
 
506
                if frame['payload'] == None:
 
507
                    # Incomplete/partial frame
 
508
                    self.traffic("}.")
 
509
                    if frame['left'] > 0:
 
510
                        self.recv_part = buf[-frame['left']:]
 
511
                    break
 
512
                else:
 
513
                    if frame['opcode'] == 0x8: # connection close
 
514
                        closed = {'code': frame['close_code'],
 
515
                                  'reason': frame['close_reason']}
 
516
                        break
 
517
 
 
518
            else:
 
519
                if buf[0:2] == s2b('\xff\x00'):
 
520
                    closed = {'code': 1000,
 
521
                              'reason': "Client sent orderly close frame"}
 
522
                    break
 
523
 
 
524
                elif buf[0:2] == s2b('\x00\xff'):
 
525
                    buf = buf[2:]
 
526
                    continue # No-op
 
527
 
 
528
                elif buf.count(s2b('\xff')) == 0:
 
529
                    # Partial frame
 
530
                    self.traffic("}.")
 
531
                    self.recv_part = buf
 
532
                    break
 
533
 
 
534
                frame = self.decode_hixie(buf)
 
535
 
 
536
            self.traffic("}")
 
537
 
 
538
            if self.rec:
 
539
                start = frame['hlen']
 
540
                end = frame['hlen'] + frame['length']
 
541
                if frame['masked']:
 
542
                    recbuf = WebSocketServer.unmask(buf, frame['hlen'],
 
543
                                                   frame['length'])
 
544
                else:
 
545
                    recbuf = buf[frame['hlen']:frame['hlen'] +
 
546
                                               frame['length']]
 
547
                self.rec.write("%s,\n" %
 
548
                        repr("}%s}" % tdelta + recbuf))
 
549
 
 
550
 
 
551
            bufs.append(frame['payload'])
 
552
 
 
553
            if frame['left']:
 
554
                buf = buf[-frame['left']:]
 
555
            else:
 
556
                buf = ''
 
557
 
 
558
        return bufs, closed
 
559
 
 
560
    def send_close(self, code=1000, reason=''):
 
561
        """ Send a WebSocket orderly close frame. """
 
562
 
 
563
        if self.version.startswith("hybi"):
 
564
            msg = pack(">H%ds" % len(reason), code, reason)
 
565
 
 
566
            buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)
 
567
            self.client.send(buf)
 
568
 
 
569
        elif self.version == "hixie-76":
 
570
            buf = s2b('\xff\x00')
 
571
            self.client.send(buf)
 
572
 
 
573
        # No orderly close for 75
 
574
 
 
575
    def do_websocket_handshake(self, headers, path):
 
576
        h = self.headers = headers
 
577
        self.path = path
 
578
 
 
579
        prot = 'WebSocket-Protocol'
 
580
        protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
 
581
 
 
582
        ver = h.get('Sec-WebSocket-Version')
 
583
        if ver:
 
584
            # HyBi/IETF version of the protocol
 
585
 
 
586
            # HyBi-07 report version 7
 
587
            # HyBi-08 - HyBi-12 report version 8
 
588
            # HyBi-13 reports version 13
 
589
            if ver in ['7', '8', '13']:
 
590
                self.version = "hybi-%02d" % int(ver)
 
591
            else:
 
592
                raise self.EClose('Unsupported protocol version %s' % ver)
 
593
 
 
594
            key = h['Sec-WebSocket-Key']
 
595
 
 
596
            # Choose binary if client supports it
 
597
            if 'binary' in protocols:
 
598
                self.base64 = False
 
599
            elif 'base64' in protocols:
 
600
                self.base64 = True
 
601
            else:
 
602
                raise self.EClose("Client must support 'binary' or 'base64' protocol")
 
603
 
 
604
            # Generate the hash value for the accept header
 
605
            accept = b64encode(sha1(s2b(key + self.GUID)).digest())
 
606
 
 
607
            response = self.server_handshake_hybi % b2s(accept)
 
608
            if self.base64:
 
609
                response += "Sec-WebSocket-Protocol: base64\r\n"
 
610
            else:
 
611
                response += "Sec-WebSocket-Protocol: binary\r\n"
 
612
            response += "\r\n"
 
613
 
 
614
        else:
 
615
            # Hixie version of the protocol (75 or 76)
 
616
 
 
617
            if h.get('key3'):
 
618
                trailer = self.gen_md5(h)
 
619
                pre = "Sec-"
 
620
                self.version = "hixie-76"
 
621
            else:
 
622
                trailer = ""
 
623
                pre = ""
 
624
                self.version = "hixie-75"
 
625
 
 
626
            # We only support base64 in Hixie era
 
627
            self.base64 = True
 
628
 
 
629
            response = self.server_handshake_hixie % (pre,
 
630
                    h['Origin'], pre, self.scheme, h['Host'], path)
 
631
 
 
632
            if 'base64' in protocols:
 
633
                response += "%sWebSocket-Protocol: base64\r\n" % pre
 
634
            else:
 
635
                self.msg("Warning: client does not report 'base64' protocol support")
 
636
            response += "\r\n" + trailer
 
637
 
 
638
        return response
 
639
 
 
640
 
 
641
    def do_handshake(self, sock, address):
 
642
        """
 
643
        do_handshake does the following:
 
644
        - Peek at the first few bytes from the socket.
 
645
        - If the connection is Flash policy request then answer it,
 
646
          close the socket and return.
 
647
        - If the connection is an HTTPS/SSL/TLS connection then SSL
 
648
          wrap the socket.
 
649
        - Read from the (possibly wrapped) socket.
 
650
        - If we have received a HTTP GET request and the webserver
 
651
          functionality is enabled, answer it, close the socket and
 
652
          return.
 
653
        - Assume we have a WebSockets connection, parse the client
 
654
          handshake data.
 
655
        - Send a WebSockets handshake server response.
 
656
        - Return the socket for this WebSocket client.
 
657
        """
 
658
        stype = ""
 
659
        ready = select.select([sock], [], [], 3)[0]
 
660
 
 
661
        
 
662
        if not ready:
 
663
            raise self.EClose("ignoring socket not ready")
 
664
        # Peek, but do not read the data so that we have a opportunity
 
665
        # to SSL wrap the socket first
 
666
        handshake = sock.recv(1024, socket.MSG_PEEK)
 
667
        #self.msg("Handshake [%s]" % handshake)
 
668
 
 
669
        if handshake == "":
 
670
            raise self.EClose("ignoring empty handshake")
 
671
 
 
672
        elif handshake.startswith(s2b("<policy-file-request/>")):
 
673
            # Answer Flash policy request
 
674
            handshake = sock.recv(1024)
 
675
            sock.send(s2b(self.policy_response))
 
676
            raise self.EClose("Sending flash policy response")
 
677
 
 
678
        elif handshake[0] in ("\x16", "\x80", 22, 128):
 
679
            # SSL wrap the connection
 
680
            if not ssl:
 
681
                raise self.EClose("SSL connection but no 'ssl' module")
 
682
            if not os.path.exists(self.cert):
 
683
                raise self.EClose("SSL connection but '%s' not found"
 
684
                                  % self.cert)
 
685
            retsock = None
 
686
            try:
 
687
                retsock = ssl.wrap_socket(
 
688
                        sock,
 
689
                        server_side=True,
 
690
                        certfile=self.cert,
 
691
                        keyfile=self.key)
 
692
            except ssl.SSLError:
 
693
                _, x, _ = sys.exc_info()
 
694
                if x.args[0] == ssl.SSL_ERROR_EOF:
 
695
                    if len(x.args) > 1:
 
696
                        raise self.EClose(x.args[1])
 
697
                    else:
 
698
                        raise self.EClose("Got SSL_ERROR_EOF")
 
699
                else:
 
700
                    raise
 
701
 
 
702
            self.scheme = "wss"
 
703
            stype = "SSL/TLS (wss://)"
 
704
 
 
705
        elif self.ssl_only:
 
706
            raise self.EClose("non-SSL connection received but disallowed")
 
707
 
 
708
        else:
 
709
            retsock = sock
 
710
            self.scheme = "ws"
 
711
            stype = "Plain non-SSL (ws://)"
 
712
 
 
713
        wsh = WSRequestHandler(retsock, address, not self.web)
 
714
        if wsh.last_code == 101:
 
715
            # Continue on to handle WebSocket upgrade
 
716
            pass
 
717
        elif wsh.last_code == 405:
 
718
            raise self.EClose("Normal web request received but disallowed")
 
719
        elif wsh.last_code < 200 or wsh.last_code >= 300:
 
720
            raise self.EClose(wsh.last_message)
 
721
        elif self.verbose:
 
722
            raise self.EClose(wsh.last_message)
 
723
        else:
 
724
            raise self.EClose("")
 
725
 
 
726
        response = self.do_websocket_handshake(wsh.headers, wsh.path)
 
727
 
 
728
        self.msg("%s: %s WebSocket connection" % (address[0], stype))
 
729
        self.msg("%s: Version %s, base64: '%s'" % (address[0],
 
730
            self.version, self.base64))
 
731
        if self.path != '/':
 
732
            self.msg("%s: Path: '%s'" % (address[0], self.path))
 
733
 
 
734
 
 
735
        # Send server WebSockets handshake response
 
736
        #self.msg("sending response [%s]" % response)
 
737
        retsock.send(s2b(response))
 
738
 
 
739
        # Return the WebSockets socket which may be SSL wrapped
 
740
        return retsock
 
741
 
 
742
 
 
743
    #
 
744
    # Events that can/should be overridden in sub-classes
 
745
    #
 
746
    def started(self):
 
747
        """ Called after WebSockets startup """
 
748
        self.vmsg("WebSockets server started")
 
749
 
 
750
    def poll(self):
 
751
        """ Run periodically while waiting for connections. """
 
752
        #self.vmsg("Running poll()")
 
753
        pass
 
754
 
 
755
    def fallback_SIGCHLD(self, sig, stack):
 
756
        # Reap zombies when using os.fork() (python 2.4)
 
757
        self.vmsg("Got SIGCHLD, reaping zombies")
 
758
        try:
 
759
            result = os.waitpid(-1, os.WNOHANG)
 
760
            while result[0]:
 
761
                self.vmsg("Reaped child process %s" % result[0])
 
762
                result = os.waitpid(-1, os.WNOHANG)
 
763
        except (OSError):
 
764
            pass
 
765
 
 
766
    def do_SIGINT(self, sig, stack):
 
767
        self.msg("Got SIGINT, exiting")
 
768
        sys.exit(0)
 
769
 
 
770
    def top_new_client(self, startsock, address):
 
771
        """ Do something with a WebSockets client connection. """
 
772
        # Initialize per client settings
 
773
        self.send_parts = []
 
774
        self.recv_part  = None
 
775
        self.base64     = False
 
776
        self.rec        = None
 
777
        self.start_time = int(time.time()*1000)
 
778
 
 
779
        # handler process        
 
780
        try:
 
781
            try:
 
782
                self.client = self.do_handshake(startsock, address)
 
783
 
 
784
                if self.record:
 
785
                    # Record raw frame data as JavaScript array
 
786
                    fname = "%s.%s" % (self.record,
 
787
                                        self.handler_id)
 
788
                    self.msg("opening record file: %s" % fname)
 
789
                    self.rec = open(fname, 'w+')
 
790
                    encoding = "binary"
 
791
                    if self.base64: encoding = "base64"
 
792
                    self.rec.write("var VNC_frame_encoding = '%s';\n"
 
793
                            % encoding)
 
794
                    self.rec.write("var VNC_frame_data = [\n")
 
795
 
 
796
                self.ws_connection = True
 
797
                self.new_client()
 
798
            except self.CClose:
 
799
                # Close the client
 
800
                _, exc, _ = sys.exc_info()
 
801
                if self.client:
 
802
                    self.send_close(exc.args[0], exc.args[1])
 
803
            except self.EClose:
 
804
                _, exc, _ = sys.exc_info()
 
805
                # Connection was not a WebSockets connection
 
806
                if exc.args[0]:
 
807
                    self.msg("%s: %s" % (address[0], exc.args[0]))
 
808
            except Exception:
 
809
                _, exc, _ = sys.exc_info()
 
810
                self.msg("handler exception: %s" % str(exc))
 
811
                if self.verbose:
 
812
                    self.msg(traceback.format_exc())
 
813
        finally:
 
814
            if self.rec:
 
815
                self.rec.write("'EOF'];\n")
 
816
                self.rec.close()
 
817
 
 
818
            if self.client and self.client != startsock:
 
819
                # Close the SSL wrapped socket
 
820
                # Original socket closed by caller
 
821
                self.client.close()
 
822
 
 
823
    def new_client(self):
 
824
        """ Do something with a WebSockets client connection. """
 
825
        raise("WebSocketServer.new_client() must be overloaded")
 
826
 
 
827
    def start_server(self):
 
828
        """
 
829
        Daemonize if requested. Listen for for connections. Run
 
830
        do_handshake() method for each connection. If the connection
 
831
        is a WebSockets client then call new_client() method (which must
 
832
        be overridden) for each new client connection.
 
833
        """
 
834
        lsock = self.socket(self.listen_host, self.listen_port, False, self.prefer_ipv6)
 
835
 
 
836
        if self.daemon:
 
837
            self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
 
838
 
 
839
        self.started()  # Some things need to happen after daemonizing
 
840
 
 
841
        # Allow override of SIGINT
 
842
        signal.signal(signal.SIGINT, self.do_SIGINT)
 
843
        if not multiprocessing:
 
844
            # os.fork() (python 2.4) child reaper
 
845
            signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD)
 
846
 
 
847
        last_active_time = self.launch_time
 
848
        while True:
 
849
            try:
 
850
                try:
 
851
                    self.client = None
 
852
                    startsock = None
 
853
                    pid = err = 0
 
854
                    child_count = 0
 
855
 
 
856
                    if multiprocessing and self.idle_timeout:
 
857
                        child_count = len(multiprocessing.active_children())
 
858
 
 
859
                    time_elapsed = time.time() - self.launch_time
 
860
                    if self.timeout and time_elapsed > self.timeout:
 
861
                        self.msg('listener exit due to --timeout %s'
 
862
                                % self.timeout)
 
863
                        break
 
864
 
 
865
                    if self.idle_timeout:
 
866
                        idle_time = 0
 
867
                        if child_count == 0:
 
868
                            idle_time = time.time() - last_active_time
 
869
                        else:
 
870
                            idle_time = 0
 
871
                            last_active_time = time.time()
 
872
 
 
873
                        if idle_time > self.idle_timeout and child_count == 0:
 
874
                            self.msg('listener exit due to --idle-timeout %s'
 
875
                                        % self.idle_timeout)
 
876
                            break
 
877
 
 
878
                    try:
 
879
                        self.poll()
 
880
 
 
881
                        ready = select.select([lsock], [], [], 1)[0]
 
882
                        if lsock in ready:
 
883
                            startsock, address = lsock.accept()
 
884
                        else:
 
885
                            continue
 
886
                    except Exception:
 
887
                        _, exc, _ = sys.exc_info()
 
888
                        if hasattr(exc, 'errno'):
 
889
                            err = exc.errno
 
890
                        elif hasattr(exc, 'args'):
 
891
                            err = exc.args[0]
 
892
                        else:
 
893
                            err = exc[0]
 
894
                        if err == errno.EINTR:
 
895
                            self.vmsg("Ignoring interrupted syscall")
 
896
                            continue
 
897
                        else:
 
898
                            raise
 
899
                    
 
900
                    if self.run_once:
 
901
                        # Run in same process if run_once
 
902
                        self.top_new_client(startsock, address)
 
903
                        if self.ws_connection :
 
904
                            self.msg('%s: exiting due to --run-once'
 
905
                                    % address[0])
 
906
                            break
 
907
                    elif multiprocessing:
 
908
                        self.vmsg('%s: new handler Process' % address[0])
 
909
                        p = multiprocessing.Process(
 
910
                                target=self.top_new_client,
 
911
                                args=(startsock, address))
 
912
                        p.start()
 
913
                        # child will not return
 
914
                    else:
 
915
                        # python 2.4
 
916
                        self.vmsg('%s: forking handler' % address[0])
 
917
                        pid = os.fork()
 
918
                        if pid == 0:
 
919
                            # child handler process
 
920
                            self.top_new_client(startsock, address)
 
921
                            break  # child process exits
 
922
 
 
923
                    # parent process
 
924
                    self.handler_id += 1
 
925
 
 
926
                except KeyboardInterrupt:
 
927
                    _, exc, _ = sys.exc_info()
 
928
                    print("In KeyboardInterrupt")
 
929
                    pass
 
930
                except SystemExit:
 
931
                    _, exc, _ = sys.exc_info()
 
932
                    print("In SystemExit")
 
933
                    break
 
934
                except Exception:
 
935
                    _, exc, _ = sys.exc_info()
 
936
                    self.msg("handler exception: %s" % str(exc))
 
937
                    if self.verbose:
 
938
                        self.msg(traceback.format_exc())
 
939
 
 
940
            finally:
 
941
                if startsock:
 
942
                    startsock.close()
 
943
 
 
944
        # Close listen port
 
945
        self.vmsg("Closing socket listening at %s:%s"
 
946
                % (self.listen_host, self.listen_port))
 
947
        lsock.close()
 
948
 
 
949
 
 
950
# HTTP handler with WebSocket upgrade support
 
951
class WSRequestHandler(SimpleHTTPRequestHandler):
 
952
    def __init__(self, req, addr, only_upgrade=False):
 
953
        self.only_upgrade = only_upgrade # only allow upgrades
 
954
        SimpleHTTPRequestHandler.__init__(self, req, addr, object())
 
955
 
 
956
    def do_GET(self):
 
957
        if (self.headers.get('upgrade') and
 
958
                self.headers.get('upgrade').lower() == 'websocket'):
 
959
 
 
960
            if (self.headers.get('sec-websocket-key1') or
 
961
                    self.headers.get('websocket-key1')):
 
962
                # For Hixie-76 read out the key hash
 
963
                self.headers.__setitem__('key3', self.rfile.read(8))
 
964
 
 
965
            # Just indicate that an WebSocket upgrade is needed
 
966
            self.last_code = 101
 
967
            self.last_message = "101 Switching Protocols"
 
968
        elif self.only_upgrade:
 
969
            # Normal web request responses are disabled
 
970
            self.last_code = 405
 
971
            self.last_message = "405 Method Not Allowed"
 
972
        else:
 
973
            SimpleHTTPRequestHandler.do_GET(self)
 
974
 
 
975
    def send_response(self, code, message=None):
 
976
        # Save the status code
 
977
        self.last_code = code
 
978
        SimpleHTTPRequestHandler.send_response(self, code, message)
 
979
 
 
980
    def log_message(self, f, *args):
 
981
        # Save instead of printing
 
982
        self.last_message = f % args