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