Revision e6daa90e share/websockify/websocket.py

View differences:

share/websockify/websocket.py
6 6
Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
7 7

  
8 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
9
    - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07
11 10
    - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
11
    - http://tools.ietf.org/html/rfc6455
12 12

  
13 13
You can make a cert/key with openssl using:
14 14
openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
......
16 16

  
17 17
'''
18 18

  
19
import os, sys, time, errno, signal, socket, traceback, select
19
import os, sys, time, errno, signal, socket, select, logging
20 20
import array, struct
21 21
from base64 import b64encode, b64decode
22 22

  
......
37 37
except: from SimpleHTTPServer import SimpleHTTPRequestHandler
38 38

  
39 39
# python 2.6 differences
40
try:    from hashlib import md5, sha1
41
except: from md5 import md5; from sha import sha as sha1
40
try:    from hashlib import sha1
41
except: from sha import sha as sha1
42 42

  
43 43
# python 2.5 differences
44 44
try:
......
50 50
        return struct.unpack(fmt, slice)
51 51

  
52 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')]:
53
for mod, msg in [('numpy', 'HyBi protocol will be slower'),
54
                 ('ssl', 'TLS/SSL/wss is disabled'),
55
                 ('multiprocessing', 'Multi-Processing is disabled'),
56
                 ('resource', 'daemonizing is disabled')]:
56 57
    try:
57 58
        globals()[mod] = __import__(mod)
58 59
    except ImportError:
59 60
        globals()[mod] = None
60
        print("WARNING: no '%s' module, %s is slower or disabled" % (
61
            mod, sup))
61
        print("WARNING: no '%s' module, %s" % (mod, msg))
62

  
62 63
if multiprocessing and sys.platform == 'win32':
63 64
    # make sockets pickle-able/inheritable
64 65
    import multiprocessing.reduction
65 66

  
66 67

  
67
class WebSocketServer(object):
68
# HTTP handler with WebSocket upgrade support
69
class WebSocketRequestHandler(SimpleHTTPRequestHandler):
68 70
    """
69
    WebSockets server class.
70
    Must be sub-classed with new_client method definition.
71
    WebSocket Request Handler Class, derived from SimpleHTTPRequestHandler.
72
    Must be sub-classed with new_websocket_client method definition.
73
    The request handler can be configured by setting optional
74
    attributes on the server object:
75

  
76
    * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled,
77
      only websocket is allowed.
78
    * verbose: If true, verbose logging is activated.
79
    * daemon: Running as daemon, do not write to console etc
80
    * record: Record raw frame data as JavaScript array into specified filename
81
    * run_once: Handle a single request
82
    * handler_id: A sequence number for this connection, appended to record filename
71 83
    """
72

  
73 84
    buffer_size = 65536
74 85

  
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 86
    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
90 87

  
91
    policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
88
    server_version = "WebSockify"
92 89

  
93
    # An exception before the WebSocket connection was established
94
    class EClose(Exception):
95
        pass
90
    protocol_version = "HTTP/1.1"
96 91

  
97 92
    # An exception while the WebSocket client was connected
98 93
    class CClose(Exception):
99 94
        pass
100 95

  
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)
96
    def __init__(self, req, addr, server):
97
        # Retrieve a few configuration variables from the server
98
        self.only_upgrade = getattr(server, "only_upgrade", False)
99
        self.verbose = getattr(server, "verbose", False)
100
        self.daemon = getattr(server, "daemon", False)
101
        self.record = getattr(server, "record", False)
102
        self.run_once = getattr(server, "run_once", False)
103
        self.rec        = None
104
        self.handler_id = getattr(server, "handler_id", False)
105
        self.file_only = getattr(server, "file_only", False)
106
        self.traffic = getattr(server, "traffic", False)
225 107

  
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
108
        self.logger = getattr(server, "logger", None)
109
        if self.logger is None:
110
            self.logger = WebSocketServer.get_logger()
236 111

  
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())
112
        SimpleHTTPRequestHandler.__init__(self, req, addr, server)
241 113

  
242 114
    @staticmethod
243 115
    def unmask(buf, hlen, plen):
......
254 126
                b = numpy.bitwise_xor(data, mask).tostring()
255 127

  
256 128
            if plen % 4:
257
                #print("Partial unmask")
129
                #self.msg("Partial unmask")
258 130
                mask = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
259 131
                        offset=hlen, count=(plen % 4))
260 132
                data = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
......
295 167
        elif payload_len >= 65536:
296 168
            header = pack('>BBQ', b1, 127, payload_len)
297 169

  
298
        #print("Encoded: %s" % repr(header + buf))
170
        #self.msg("Encoded: %s", repr(header + buf))
299 171

  
300 172
        return header + buf, len(header), 0
301 173

  
302 174
    @staticmethod
303
    def decode_hybi(buf, base64=False):
175
    def decode_hybi(buf, base64=False, logger=None):
304 176
        """ Decode HyBi style WebSocket packets.
305 177
        Returns:
306 178
            {'fin'          : 0_or_1,
......
324 196
             'close_code'   : 1000,
325 197
             'close_reason' : ''}
326 198

  
199
        if logger is None:
200
            logger = WebSocketServer.get_logger()
201

  
327 202
        blen = len(buf)
328 203
        f['left'] = blen
329 204

  
......
359 234
        # Process 1 frame
360 235
        if f['masked']:
361 236
            # unmask payload
362
            f['payload'] = WebSocketServer.unmask(buf, f['hlen'],
237
            f['payload'] = WebSocketRequestHandler.unmask(buf, f['hlen'],
363 238
                                                  f['length'])
364 239
        else:
365
            print("Unmasked frame: %s" % repr(buf))
240
            logger.debug("Unmasked frame: %s" % repr(buf))
366 241
            f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len]
367 242

  
368 243
        if base64 and f['opcode'] in [1, 2]:
369 244
            try:
370 245
                f['payload'] = b64decode(f['payload'])
371 246
            except:
372
                print("Exception while b64decoding buffer: %s" %
373
                        repr(buf))
247
                logger.exception("Exception while b64decoding buffer: %s" %
248
                                 (repr(buf)))
374 249
                raise
375 250

  
376 251
        if f['opcode'] == 0x08:
......
381 256

  
382 257
        return f
383 258

  
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 259

  
412 260
    #
413
    # WebSocketServer logging/output functions
261
    # WebSocketRequestHandler logging/output functions
414 262
    #
415 263

  
416
    def traffic(self, token="."):
417
        """ Show traffic flow in verbose mode. """
418
        if self.verbose and not self.daemon:
264
    def print_traffic(self, token="."):
265
        """ Show traffic flow mode. """
266
        if self.traffic:
419 267
            sys.stdout.write(token)
420 268
            sys.stdout.flush()
421 269

  
422
    def msg(self, msg):
270
    def msg(self, msg, *args, **kwargs):
423 271
        """ Output message with handler_id prefix. """
424
        if not self.daemon:
425
            print("% 3d: %s" % (self.handler_id, msg))
272
        prefix = "% 3d: " % self.handler_id
273
        self.logger.log(logging.INFO, "%s%s" % (prefix, msg), *args, **kwargs)
426 274

  
427
    def vmsg(self, msg):
428
        """ Same as msg() but only if verbose. """
429
        if self.verbose:
430
            self.msg(msg)
275
    def vmsg(self, msg, *args, **kwargs):
276
        """ Same as msg() but as debug. """
277
        prefix = "% 3d: " % self.handler_id
278
        self.logger.log(logging.DEBUG, "%s%s" % (prefix, msg), *args, **kwargs)
279

  
280
    def warn(self, msg, *args, **kwargs):
281
        """ Same as msg() but as warning. """
282
        prefix = "% 3d: " % self.handler_id
283
        self.logger.log(logging.WARN, "%s%s" % (prefix, msg), *args, **kwargs)
431 284

  
432 285
    #
433
    # Main WebSocketServer methods
286
    # Main WebSocketRequestHandler methods
434 287
    #
435 288
    def send_frames(self, bufs=None):
436 289
        """ Encode and send WebSocket frames. Any frames already
......
444 297

  
445 298
        if bufs:
446 299
            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

  
300
                if self.base64:
301
                    encbuf, lenhead, lentail = self.encode_hybi(buf, opcode=1, base64=True)
455 302
                else:
456
                    encbuf, lenhead, lentail = self.encode_hixie(buf)
303
                    encbuf, lenhead, lentail = self.encode_hybi(buf, opcode=2, base64=False)
457 304

  
458 305
                if self.rec:
459 306
                    self.rec.write("%s,\n" %
......
465 312
        while self.send_parts:
466 313
            # Send pending frames
467 314
            buf = self.send_parts.pop(0)
468
            sent = self.client.send(buf)
315
            sent = self.request.send(buf)
469 316

  
470 317
            if sent == len(buf):
471
                self.traffic("<")
318
                self.print_traffic("<")
472 319
            else:
473
                self.traffic("<.")
320
                self.print_traffic("<.")
474 321
                self.send_parts.insert(0, buf[sent:])
475 322
                break
476 323

  
......
487 334
        bufs = []
488 335
        tdelta = int(time.time()*1000) - self.start_time
489 336

  
490
        buf = self.client.recv(self.buffer_size)
337
        buf = self.request.recv(self.buffer_size)
491 338
        if len(buf) == 0:
492 339
            closed = {'code': 1000, 'reason': "Client closed abruptly"}
493 340
            return bufs, closed
......
498 345
            self.recv_part = None
499 346

  
500 347
        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

  
348
            frame = self.decode_hybi(buf, base64=self.base64,
349
                                     logger=self.logger)
350
            #self.msg("Received buf: %s, frame: %s", repr(buf), frame)
351

  
352
            if frame['payload'] == None:
353
                # Incomplete/partial frame
354
                self.print_traffic("}.")
355
                if frame['left'] > 0:
356
                    self.recv_part = buf[-frame['left']:]
357
                break
518 358
            else:
519
                if buf[0:2] == s2b('\xff\x00'):
520
                    closed = {'code': 1000,
521
                              'reason': "Client sent orderly close frame"}
359
                if frame['opcode'] == 0x8: # connection close
360
                    closed = {'code': frame['close_code'],
361
                              'reason': frame['close_reason']}
522 362
                    break
523 363

  
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("}")
364
            self.print_traffic("}")
537 365

  
538 366
            if self.rec:
539 367
                start = frame['hlen']
540 368
                end = frame['hlen'] + frame['length']
541 369
                if frame['masked']:
542
                    recbuf = WebSocketServer.unmask(buf, frame['hlen'],
370
                    recbuf = WebSocketRequestHandler.unmask(buf, frame['hlen'],
543 371
                                                   frame['length'])
544 372
                else:
545 373
                    recbuf = buf[frame['hlen']:frame['hlen'] +
......
560 388
    def send_close(self, code=1000, reason=''):
561 389
        """ Send a WebSocket orderly close frame. """
562 390

  
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
391
        msg = pack(">H%ds" % len(reason), code, reason)
392
        buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)
393
        self.request.send(buf)
574 394

  
575
    def do_websocket_handshake(self, headers, path):
576
        h = self.headers = headers
577
        self.path = path
395
    def do_websocket_handshake(self):
396
        h = self.headers
578 397

  
579 398
        prot = 'WebSocket-Protocol'
580 399
        protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
......
589 408
            if ver in ['7', '8', '13']:
590 409
                self.version = "hybi-%02d" % int(ver)
591 410
            else:
592
                raise self.EClose('Unsupported protocol version %s' % ver)
411
                self.send_error(400, "Unsupported protocol version %s" % ver)
412
                return False
593 413

  
594 414
            key = h['Sec-WebSocket-Key']
595 415

  
......
599 419
            elif 'base64' in protocols:
600 420
                self.base64 = True
601 421
            else:
602
                raise self.EClose("Client must support 'binary' or 'base64' protocol")
422
                self.send_error(400, "Client must support 'binary' or 'base64' protocol")
423
                return False
603 424

  
604 425
            # Generate the hash value for the accept header
605 426
            accept = b64encode(sha1(s2b(key + self.GUID)).digest())
606 427

  
607
            response = self.server_handshake_hybi % b2s(accept)
428
            self.send_response(101, "Switching Protocols")
429
            self.send_header("Upgrade", "websocket")
430
            self.send_header("Connection", "Upgrade")
431
            self.send_header("Sec-WebSocket-Accept", b2s(accept))
608 432
            if self.base64:
609
                response += "Sec-WebSocket-Protocol: base64\r\n"
433
                self.send_header("Sec-WebSocket-Protocol", "base64")
434
            else:
435
                self.send_header("Sec-WebSocket-Protocol", "binary")
436
            self.end_headers()
437
            return True
438
        else:
439
            self.send_error(400, "Missing Sec-WebSocket-Version header. Hixie protocols not supported.")
440

  
441
        return False
442

  
443
    def handle_websocket(self):
444
        """Upgrade a connection to Websocket, if requested. If this succeeds,
445
        new_websocket_client() will be called. Otherwise, False is returned.
446
        """
447
        if (self.headers.get('upgrade') and
448
            self.headers.get('upgrade').lower() == 'websocket'):
449

  
450
            if not self.do_websocket_handshake():
451
                return False
452

  
453
            # Indicate to server that a Websocket upgrade was done
454
            self.server.ws_connection = True
455
            # Initialize per client settings
456
            self.send_parts = []
457
            self.recv_part  = None
458
            self.start_time = int(time.time()*1000)
459

  
460
            # client_address is empty with, say, UNIX domain sockets
461
            client_addr = ""
462
            is_ssl = False
463
            try:
464
                client_addr = self.client_address[0]
465
                is_ssl = self.client_address[2]
466
            except IndexError:
467
                pass
468

  
469
            if is_ssl:
470
                self.stype = "SSL/TLS (wss://)"
610 471
            else:
611
                response += "Sec-WebSocket-Protocol: binary\r\n"
612
            response += "\r\n"
472
                self.stype = "Plain non-SSL (ws://)"
473

  
474
            self.log_message("%s: %s WebSocket connection", client_addr,
475
                             self.stype)
476
            self.log_message("%s: Version %s, base64: '%s'", client_addr,
477
                             self.version, self.base64)
478
            if self.path != '/':
479
                self.log_message("%s: Path: '%s'", client_addr, self.path)
480

  
481
            if self.record:
482
                # Record raw frame data as JavaScript array
483
                fname = "%s.%s" % (self.record,
484
                                   self.handler_id)
485
                self.log_message("opening record file: %s", fname)
486
                self.rec = open(fname, 'w+')
487
                encoding = "binary"
488
                if self.base64: encoding = "base64"
489
                self.rec.write("var VNC_frame_encoding = '%s';\n"
490
                               % encoding)
491
                self.rec.write("var VNC_frame_data = [\n")
613 492

  
493
            try:
494
                self.new_websocket_client()
495
            except self.CClose:
496
                # Close the client
497
                _, exc, _ = sys.exc_info()
498
                self.send_close(exc.args[0], exc.args[1])
499
            return True
614 500
        else:
615
            # Hixie version of the protocol (75 or 76)
501
            return False
502

  
503
    def do_GET(self):
504
        """Handle GET request. Calls handle_websocket(). If unsuccessful,
505
        and web server is enabled, SimpleHTTPRequestHandler.do_GET will be called."""
506
        if not self.handle_websocket():
507
            if self.only_upgrade:
508
                self.send_error(405, "Method Not Allowed")
509
            else:
510
                SimpleHTTPRequestHandler.do_GET(self)
511

  
512
    def list_directory(self, path):
513
        if self.file_only:
514
            self.send_error(404, "No such file")
515
        else:
516
            return SimpleHTTPRequestHandler.list_directory(self, path)
517

  
518
    def new_websocket_client(self):
519
        """ Do something with a WebSockets client connection. """
520
        raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded")
521

  
522
    def do_HEAD(self):
523
        if self.only_upgrade:
524
            self.send_error(405, "Method Not Allowed")
525
        else:
526
            SimpleHTTPRequestHandler.do_HEAD(self)
527

  
528
    def finish(self):
529
        if self.rec:
530
            self.rec.write("'EOF'];\n")
531
            self.rec.close()
532

  
533
    def handle(self):
534
        # When using run_once, we have a single process, so
535
        # we cannot loop in BaseHTTPRequestHandler.handle; we
536
        # must return and handle new connections
537
        if self.run_once:
538
            self.handle_one_request()
539
        else:
540
            SimpleHTTPRequestHandler.handle(self)
541

  
542
    def log_request(self, code='-', size='-'):
543
        if self.verbose:
544
            SimpleHTTPRequestHandler.log_request(self, code, size)
545

  
616 546

  
617
            if h.get('key3'):
618
                trailer = self.gen_md5(h)
619
                pre = "Sec-"
620
                self.version = "hixie-76"
547
class WebSocketServer(object):
548
    """
549
    WebSockets server class.
550
    As an alternative, the standard library SocketServer can be used
551
    """
552

  
553
    policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
554
    log_prefix = "websocket"
555

  
556
    # An exception before the WebSocket connection was established
557
    class EClose(Exception):
558
        pass
559

  
560
    class Terminate(Exception):
561
        pass
562

  
563
    def __init__(self, RequestHandlerClass, listen_host='',
564
                 listen_port=None, source_is_ipv6=False,
565
            verbose=False, cert='', key='', ssl_only=None,
566
            daemon=False, record='', web='',
567
            file_only=False,
568
            run_once=False, timeout=0, idle_timeout=0, traffic=False,
569
            tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None,
570
            tcp_keepintvl=None):
571

  
572
        # settings
573
        self.RequestHandlerClass = RequestHandlerClass
574
        self.verbose        = verbose
575
        self.listen_host    = listen_host
576
        self.listen_port    = listen_port
577
        self.prefer_ipv6    = source_is_ipv6
578
        self.ssl_only       = ssl_only
579
        self.daemon         = daemon
580
        self.run_once       = run_once
581
        self.timeout        = timeout
582
        self.idle_timeout   = idle_timeout
583
        self.traffic        = traffic
584

  
585
        self.launch_time    = time.time()
586
        self.ws_connection  = False
587
        self.handler_id     = 1
588

  
589
        self.logger         = self.get_logger()
590
        self.tcp_keepalive  = tcp_keepalive
591
        self.tcp_keepcnt    = tcp_keepcnt
592
        self.tcp_keepidle   = tcp_keepidle
593
        self.tcp_keepintvl  = tcp_keepintvl
594

  
595
        # Make paths settings absolute
596
        self.cert = os.path.abspath(cert)
597
        self.key = self.web = self.record = ''
598
        if key:
599
            self.key = os.path.abspath(key)
600
        if web:
601
            self.web = os.path.abspath(web)
602
        if record:
603
            self.record = os.path.abspath(record)
604

  
605
        if self.web:
606
            os.chdir(self.web)
607
        self.only_upgrade = not self.web
608

  
609
        # Sanity checks
610
        if not ssl and self.ssl_only:
611
            raise Exception("No 'ssl' module and SSL-only specified")
612
        if self.daemon and not resource:
613
            raise Exception("Module 'resource' required to daemonize")
614

  
615
        # Show configuration
616
        self.msg("WebSocket server settings:")
617
        self.msg("  - Listen on %s:%s",
618
                self.listen_host, self.listen_port)
619
        self.msg("  - Flash security policy server")
620
        if self.web:
621
            self.msg("  - Web server. Web root: %s", self.web)
622
        if ssl:
623
            if os.path.exists(self.cert):
624
                self.msg("  - SSL/TLS support")
625
                if self.ssl_only:
626
                    self.msg("  - Deny non-SSL/TLS connections")
621 627
            else:
622
                trailer = ""
623
                pre = ""
624
                self.version = "hixie-75"
628
                self.msg("  - No SSL/TLS support (no cert file)")
629
        else:
630
            self.msg("  - No SSL/TLS support (no 'ssl' module)")
631
        if self.daemon:
632
            self.msg("  - Backgrounding (daemon)")
633
        if self.record:
634
            self.msg("  - Recording to '%s.*'", self.record)
625 635

  
626
            # We only support base64 in Hixie era
627
            self.base64 = True
636
    #
637
    # WebSocketServer static methods
638
    #
628 639

  
629
            response = self.server_handshake_hixie % (pre,
630
                    h['Origin'], pre, self.scheme, h['Host'], path)
640
    @staticmethod
641
    def get_logger():
642
        return logging.getLogger("%s.%s" % (
643
            WebSocketServer.log_prefix,
644
            WebSocketServer.__class__.__name__))
631 645

  
632
            if 'base64' in protocols:
633
                response += "%sWebSocket-Protocol: base64\r\n" % pre
646
    @staticmethod
647
    def socket(host, port=None, connect=False, prefer_ipv6=False,
648
               unix_socket=None, use_ssl=False, tcp_keepalive=True,
649
               tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None):
650
        """ Resolve a host (and optional port) to an IPv4 or IPv6
651
        address. Create a socket. Bind to it if listen is set,
652
        otherwise connect to it. Return the socket.
653
        """
654
        flags = 0
655
        if host == '':
656
            host = None
657
        if connect and not (port or unix_socket):
658
            raise Exception("Connect mode requires a port")
659
        if use_ssl and not ssl:
660
            raise Exception("SSL socket requested but Python SSL module not loaded.");
661
        if not connect and use_ssl:
662
            raise Exception("SSL only supported in connect mode (for now)")
663
        if not connect:
664
            flags = flags | socket.AI_PASSIVE
665

  
666
        if not unix_socket:
667
            addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
668
                    socket.IPPROTO_TCP, flags)
669
            if not addrs:
670
                raise Exception("Could not resolve host '%s'" % host)
671
            addrs.sort(key=lambda x: x[0])
672
            if prefer_ipv6:
673
                addrs.reverse()
674
            sock = socket.socket(addrs[0][0], addrs[0][1])
675

  
676
            if  tcp_keepalive:
677
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
678
                if tcp_keepcnt:
679
                    sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT,
680
                                    tcp_keepcnt)
681
                if tcp_keepidle:
682
                    sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE,
683
                                    tcp_keepidle)
684
                if tcp_keepintvl:
685
                    sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL,
686
                                    tcp_keepintvl)
687

  
688
            if connect:
689
                sock.connect(addrs[0][4])
690
                if use_ssl:
691
                    sock = ssl.wrap_socket(sock)
634 692
            else:
635
                self.msg("Warning: client does not report 'base64' protocol support")
636
            response += "\r\n" + trailer
693
                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
694
                sock.bind(addrs[0][4])
695
                sock.listen(100)
696
        else:
697
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
698
            sock.connect(unix_socket)
699

  
700
        return sock
637 701

  
638
        return response
702
    @staticmethod
703
    def daemonize(keepfd=None, chdir='/'):
704
        os.umask(0)
705
        if chdir:
706
            os.chdir(chdir)
707
        else:
708
            os.chdir('/')
709
        os.setgid(os.getgid())  # relinquish elevations
710
        os.setuid(os.getuid())  # relinquish elevations
639 711

  
712
        # Double fork to daemonize
713
        if os.fork() > 0: os._exit(0)  # Parent exits
714
        os.setsid()                    # Obtain new process group
715
        if os.fork() > 0: os._exit(0)  # Parent exits
716

  
717
        # Signal handling
718
        signal.signal(signal.SIGTERM, signal.SIG_IGN)
719
        signal.signal(signal.SIGINT, signal.SIG_IGN)
720

  
721
        # Close open files
722
        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
723
        if maxfd == resource.RLIM_INFINITY: maxfd = 256
724
        for fd in reversed(range(maxfd)):
725
            try:
726
                if fd != keepfd:
727
                    os.close(fd)
728
            except OSError:
729
                _, exc, _ = sys.exc_info()
730
                if exc.errno != errno.EBADF: raise
731

  
732
        # Redirect I/O to /dev/null
733
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno())
734
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno())
735
        os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno())
640 736

  
641 737
    def do_handshake(self, sock, address):
642 738
        """
......
655 751
        - Send a WebSockets handshake server response.
656 752
        - Return the socket for this WebSocket client.
657 753
        """
658
        stype = ""
659 754
        ready = select.select([sock], [], [], 3)[0]
660 755

  
661 756
        
......
699 794
                else:
700 795
                    raise
701 796

  
702
            self.scheme = "wss"
703
            stype = "SSL/TLS (wss://)"
704

  
705 797
        elif self.ssl_only:
706 798
            raise self.EClose("non-SSL connection received but disallowed")
707 799

  
708 800
        else:
709 801
            retsock = sock
710
            self.scheme = "ws"
711
            stype = "Plain non-SSL (ws://)"
712 802

  
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("")
803
        # If the address is like (host, port), we are extending it
804
        # with a flag indicating SSL. Not many other options
805
        # available...
806
        if len(address) == 2:
807
            address = (address[0], address[1], (retsock != sock))
725 808

  
726
        response = self.do_websocket_handshake(wsh.headers, wsh.path)
809
        self.RequestHandlerClass(retsock, address, self)
727 810

  
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))
811
        # Return the WebSockets socket which may be SSL wrapped
812
        return retsock
733 813

  
814
    #
815
    # WebSocketServer logging/output functions
816
    #
734 817

  
735
        # Send server WebSockets handshake response
736
        #self.msg("sending response [%s]" % response)
737
        retsock.send(s2b(response))
818
    def msg(self, *args, **kwargs):
819
        """ Output message as info """
820
        self.logger.log(logging.INFO, *args, **kwargs)
738 821

  
739
        # Return the WebSockets socket which may be SSL wrapped
740
        return retsock
822
    def vmsg(self, *args, **kwargs):
823
        """ Same as msg() but as debug. """
824
        self.logger.log(logging.DEBUG, *args, **kwargs)
825

  
826
    def warn(self, *args, **kwargs):
827
        """ Same as msg() but as warning. """
828
        self.logger.log(logging.WARN, *args, **kwargs)
741 829

  
742 830

  
743 831
    #
......
752 840
        #self.vmsg("Running poll()")
753 841
        pass
754 842

  
843
    def terminate(self):
844
        raise self.Terminate()
845

  
846
    def multiprocessing_SIGCHLD(self, sig, stack):
847
        self.vmsg('Reaping zombies, active child count is %s', len(multiprocessing.active_children()))
848

  
755 849
    def fallback_SIGCHLD(self, sig, stack):
756 850
        # Reap zombies when using os.fork() (python 2.4)
757 851
        self.vmsg("Got SIGCHLD, reaping zombies")
......
765 859

  
766 860
    def do_SIGINT(self, sig, stack):
767 861
        self.msg("Got SIGINT, exiting")
768
        sys.exit(0)
862
        self.terminate()
863

  
864
    def do_SIGTERM(self, sig, stack):
865
        self.msg("Got SIGTERM, exiting")
866
        self.terminate()
769 867

  
770 868
    def top_new_client(self, startsock, address):
771 869
        """ 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 870
        # handler process        
871
        client = None
780 872
        try:
781 873
            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])
874
                client = self.do_handshake(startsock, address)
803 875
            except self.EClose:
804 876
                _, exc, _ = sys.exc_info()
805 877
                # Connection was not a WebSockets connection
806 878
                if exc.args[0]:
807 879
                    self.msg("%s: %s" % (address[0], exc.args[0]))
880
            except WebSocketServer.Terminate:
881
                raise
808 882
            except Exception:
809 883
                _, exc, _ = sys.exc_info()
810 884
                self.msg("handler exception: %s" % str(exc))
811
                if self.verbose:
812
                    self.msg(traceback.format_exc())
885
                self.vmsg("exception", exc_info=True)
813 886
        finally:
814
            if self.rec:
815
                self.rec.write("'EOF'];\n")
816
                self.rec.close()
817 887

  
818
            if self.client and self.client != startsock:
888
            if client and client != startsock:
819 889
                # Close the SSL wrapped socket
820 890
                # 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")
891
                client.close()
826 892

  
827 893
    def start_server(self):
828 894
        """
829 895
        Daemonize if requested. Listen for for connections. Run
830 896
        do_handshake() method for each connection. If the connection
831
        is a WebSockets client then call new_client() method (which must
897
        is a WebSockets client then call new_websocket_client() method (which must
832 898
        be overridden) for each new client connection.
833 899
        """
834
        lsock = self.socket(self.listen_host, self.listen_port, False, self.prefer_ipv6)
900
        lsock = self.socket(self.listen_host, self.listen_port, False,
901
                            self.prefer_ipv6,
902
                            tcp_keepalive=self.tcp_keepalive,
903
                            tcp_keepcnt=self.tcp_keepcnt,
904
                            tcp_keepidle=self.tcp_keepidle,
905
                            tcp_keepintvl=self.tcp_keepintvl)
835 906

  
836 907
        if self.daemon:
837 908
            self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
838 909

  
839 910
        self.started()  # Some things need to happen after daemonizing
840 911

  
841
        # Allow override of SIGINT
912
        # Allow override of signals
913
        original_signals = {
914
            signal.SIGINT: signal.getsignal(signal.SIGINT),
915
            signal.SIGTERM: signal.getsignal(signal.SIGTERM),
916
            signal.SIGCHLD: signal.getsignal(signal.SIGCHLD),
917
        }
842 918
        signal.signal(signal.SIGINT, self.do_SIGINT)
919
        signal.signal(signal.SIGTERM, self.do_SIGTERM)
843 920
        if not multiprocessing:
844 921
            # os.fork() (python 2.4) child reaper
845 922
            signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD)
923
        else:
924
            # make sure that _cleanup is called when children die
925
            # by calling active_children on SIGCHLD
926
            signal.signal(signal.SIGCHLD, self.multiprocessing_SIGCHLD)
846 927

  
847 928
        last_active_time = self.launch_time
848
        while True:
849
            try:
929
        try:
930
            while True:
850 931
                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 932
                    try:
879
                        self.poll()
933
                        startsock = None
934
                        pid = err = 0
935
                        child_count = 0
936

  
937
                        if multiprocessing:
938
                            # Collect zombie child processes
939
                            child_count = len(multiprocessing.active_children())
940

  
941
                        time_elapsed = time.time() - self.launch_time
942
                        if self.timeout and time_elapsed > self.timeout:
943
                            self.msg('listener exit due to --timeout %s'
944
                                    % self.timeout)
945
                            break
880 946

  
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:
947
                        if self.idle_timeout:
948
                            idle_time = 0
949
                            if child_count == 0:
950
                                idle_time = time.time() - last_active_time
951
                            else:
952
                                idle_time = 0
953
                                last_active_time = time.time()
954

  
955
                            if idle_time > self.idle_timeout and child_count == 0:
956
                                self.msg('listener exit due to --idle-timeout %s'
957
                                            % self.idle_timeout)
958
                                break
959

  
960
                        try:
961
                            self.poll()
962

  
963
                            ready = select.select([lsock], [], [], 1)[0]
964
                            if lsock in ready:
965
                                startsock, address = lsock.accept()
966
                            else:
967
                                continue
968
                        except self.Terminate:
898 969
                            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
970
                        except Exception:
971
                            _, exc, _ = sys.exc_info()
972
                            if hasattr(exc, 'errno'):
973
                                err = exc.errno
974
                            elif hasattr(exc, 'args'):
975
                                err = exc.args[0]
976
                            else:
977
                                err = exc[0]
978
                            if err == errno.EINTR:
979
                                self.vmsg("Ignoring interrupted syscall")
980
                                continue
981
                            else:
982
                                raise
983

  
984
                        if self.run_once:
985
                            # Run in same process if run_once
920 986
                            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

  
987
                            if self.ws_connection :
988
                                self.msg('%s: exiting due to --run-once'
989
                                        % address[0])
990
                                break
991
                        elif multiprocessing:
992
                            self.vmsg('%s: new handler Process' % address[0])
993
                            p = multiprocessing.Process(
994
                                    target=self.top_new_client,
995
                                    args=(startsock, address))
996
                            p.start()
997
                            # child will not return
998
                        else:
999
                            # python 2.4
1000
                            self.vmsg('%s: forking handler' % address[0])
1001
                            pid = os.fork()
1002
                            if pid == 0:
1003
                                # child handler process
1004
                                self.top_new_client(startsock, address)
1005
                                break  # child process exits
1006

  
1007
                        # parent process
1008
                        self.handler_id += 1
1009

  
1010
                    except (self.Terminate, SystemExit, KeyboardInterrupt):
1011
                        self.msg("In exit")
1012
                        break
1013
                    except Exception:
1014
                        self.msg("handler exception: %s", str(exc))
1015
                        self.vmsg("exception", exc_info=True)
944 1016

  
945
# HTTP handler with WebSocket upgrade support
946
class WSRequestHandler(SimpleHTTPRequestHandler):
947
    def __init__(self, req, addr, only_upgrade=False):
948
        self.only_upgrade = only_upgrade # only allow upgrades
949
        SimpleHTTPRequestHandler.__init__(self, req, addr, object())
1017
                finally:
1018
                    if startsock:
1019
                        startsock.close()
1020
        finally:
1021
            # Close listen port
1022
            self.vmsg("Closing socket listening at %s:%s",
1023
                      self.listen_host, self.listen_port)
1024
            lsock.close()
950 1025

  
951
    def do_GET(self):
952
        if (self.headers.get('upgrade') and
953
                self.headers.get('upgrade').lower() == 'websocket'):
954

  
955
            if (self.headers.get('sec-websocket-key1') or
956
                    self.headers.get('websocket-key1')):
957
                # For Hixie-76 read out the key hash
958
                self.headers.__setitem__('key3', self.rfile.read(8))
959

  
960
            # Just indicate that an WebSocket upgrade is needed
961
            self.last_code = 101
962
            self.last_message = "101 Switching Protocols"
963
        elif self.only_upgrade:
964
            # Normal web request responses are disabled
965
            self.last_code = 405
966
            self.last_message = "405 Method Not Allowed"
967
        else:
968
            SimpleHTTPRequestHandler.do_GET(self)
1026
            # Restore signals
1027
            for sig, func in original_signals.items():
1028
                signal.signal(sig, func)
969 1029

  
970
    def send_response(self, code, message=None):
971
        # Save the status code
972
        self.last_code = code
973
        SimpleHTTPRequestHandler.send_response(self, code, message)
974 1030

  
975
    def log_message(self, f, *args):
976
        # Save instead of printing
977
        self.last_message = f % args

Also available in: Unified diff