]> git.proxmox.com Git - mirror_novnc.git/blame - utils/websocket.py
Merge pull request #252 from astrand/port-80-443
[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
48f26d79 11 - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
7d146027 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
1e508715 19import os, sys, time, errno, signal, socket, traceback, select
f2d85676 20import array, struct
95ef30a1 21from base64 import b64encode, b64decode
8c305c60
JM
22
23# Imports that vary by python version
f2d85676
JM
24
25# python 3.0 differences
8c305c60 26if sys.hexversion > 0x3000000:
8c305c60
JM
27 b2s = lambda buf: buf.decode('latin_1')
28 s2b = lambda s: s.encode('latin_1')
1e508715 29 s2a = lambda s: s
8c305c60 30else:
f2d85676
JM
31 b2s = lambda buf: buf # No-op
32 s2b = lambda s: s # No-op
1e508715 33 s2a = lambda s: [ord(c) for c in s]
f2d85676
JM
34try: from io import StringIO
35except: from cStringIO import StringIO
36try: from http.server import SimpleHTTPRequestHandler
37except: from SimpleHTTPServer import SimpleHTTPRequestHandler
f2d85676
JM
38
39# python 2.6 differences
40try: from hashlib import md5, sha1
41except: from md5 import md5; from sha import sha as sha1
42
43# python 2.5 differences
44try:
45 from struct import pack, unpack_from
46except:
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)
8c305c60
JM
51
52# Degraded functionality if these imports are missing
f2d85676
JM
53for mod, sup in [('numpy', 'HyBi protocol'), ('ssl', 'TLS/SSL/wss'),
54 ('multiprocessing', 'Multi-Processing'),
55 ('resource', 'daemonizing')]:
8c305c60
JM
56 try:
57 globals()[mod] = __import__(mod)
58 except ImportError:
59 globals()[mod] = None
f2d85676 60 print("WARNING: no '%s' module, %s is slower or disabled" % (
8c305c60 61 mod, sup))
f2d85676
JM
62if multiprocessing and sys.platform == 'win32':
63 # make sockets pickle-able/inheritable
64 import multiprocessing.reduction
8c305c60 65
95ef30a1 66
66937e39 67class WebSocketServer(object):
6a883409
JM
68 """
69 WebSockets server class.
f2538f33 70 Must be sub-classed with new_client method definition.
6a883409
JM
71 """
72
7d146027
JM
73 buffer_size = 65536
74
204675c8 75
7d146027 76 server_handshake_hixie = """HTTP/1.1 101 Web Socket Protocol Handshake\r
95ef30a1
JM
77Upgrade: WebSocket\r
78Connection: Upgrade\r
486cd527
JM
79%sWebSocket-Origin: %s\r
80%sWebSocket-Location: %s://%s%s\r
7d146027
JM
81"""
82
83 server_handshake_hybi = """HTTP/1.1 101 Switching Protocols\r
84Upgrade: websocket\r
85Connection: Upgrade\r
86Sec-WebSocket-Accept: %s\r
87"""
88
89 GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
95ef30a1 90
6a883409
JM
91 policy_response = """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
92
c3900110 93 # An exception before the WebSocket connection was established
6a883409
JM
94 class EClose(Exception):
95 pass
96
c3900110
JM
97 # An exception while the WebSocket client was connected
98 class CClose(Exception):
99 pass
100
123e5e74 101 def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False,
6a883409 102 verbose=False, cert='', key='', ssl_only=None,
1e508715 103 daemon=False, record='', web='',
204675c8 104 run_once=False, timeout=0, idle_timeout=0):
6a883409
JM
105
106 # settings
123e5e74
JM
107 self.verbose = verbose
108 self.listen_host = listen_host
109 self.listen_port = listen_port
204675c8 110 self.prefer_ipv6 = source_is_ipv6
123e5e74
JM
111 self.ssl_only = ssl_only
112 self.daemon = daemon
1e508715
JM
113 self.run_once = run_once
114 self.timeout = timeout
204675c8
JM
115 self.idle_timeout = idle_timeout
116
1e508715
JM
117 self.launch_time = time.time()
118 self.ws_connection = False
123e5e74 119 self.handler_id = 1
6a883409 120
6a883409
JM
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
8c305c60 134 # Sanity checks
123e5e74 135 if not ssl and self.ssl_only:
8c305c60
JM
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")
f2538f33 145 if self.web:
c0c143a1 146 print(" - Web server. Web root: %s" % self.web)
8c305c60
JM
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)")
f2538f33 154 else:
8c305c60 155 print(" - No SSL/TLS support (no 'ssl' module)")
f2538f33 156 if self.daemon:
8c305c60
JM
157 print(" - Backgrounding (daemon)")
158 if self.record:
159 print(" - Recording to '%s.*'" % self.record)
f2538f33 160
6a883409
JM
161 #
162 # WebSocketServer static methods
163 #
3a39bf60 164
123e5e74 165 @staticmethod
204675c8 166 def socket(host, port=None, connect=False, prefer_ipv6=False, unix_socket=None, use_ssl=False):
4f8c7465 167 """ Resolve a host (and optional port) to an IPv4 or IPv6
c0c143a1
JM
168 address. Create a socket. Bind to it if listen is set,
169 otherwise connect to it. Return the socket.
123e5e74 170 """
4f8c7465 171 flags = 0
c0c143a1
JM
172 if host == '':
173 host = None
204675c8 174 if connect and not (port or unix_socket):
c0c143a1 175 raise Exception("Connect mode requires a port")
204675c8
JM
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)")
4f8c7465
JM
180 if not connect:
181 flags = flags | socket.AI_PASSIVE
204675c8
JM
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
4f8c7465 204 return sock
123e5e74 205
6a883409 206 @staticmethod
7d146027 207 def daemonize(keepfd=None, chdir='/'):
6a883409 208 os.umask(0)
7d146027
JM
209 if chdir:
210 os.chdir(chdir)
6a883409
JM
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)
8c305c60
JM
233 except OSError:
234 _, exc, _ = sys.exc_info()
6a883409
JM
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
1e508715 242 @staticmethod
49578da4
JM
243 def unmask(buf, hlen, plen):
244 pstart = hlen + 4
245 pend = pstart + plen
1e508715
JM
246 if numpy:
247 b = c = s2b('')
49578da4 248 if plen >= 4:
1e508715 249 mask = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'),
49578da4 250 offset=hlen, count=1)
1e508715 251 data = numpy.frombuffer(buf, dtype=numpy.dtype('<u4'),
49578da4 252 offset=pstart, count=int(plen / 4))
1e508715
JM
253 #b = numpy.bitwise_xor(data, mask).data
254 b = numpy.bitwise_xor(data, mask).tostring()
255
49578da4 256 if plen % 4:
1e508715
JM
257 #print("Partial unmask")
258 mask = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
49578da4 259 offset=hlen, count=(plen % 4))
1e508715 260 data = numpy.frombuffer(buf, dtype=numpy.dtype('B'),
49578da4
JM
261 offset=pend - (plen % 4),
262 count=(plen % 4))
1e508715
JM
263 c = numpy.bitwise_xor(data, mask).tostring()
264 return b + c
265 else:
266 # Slower fallback
49578da4 267 mask = buf[hlen:hlen+4]
1e508715 268 data = array.array('B')
49578da4 269 mask = s2a(mask)
1e508715
JM
270 data.fromstring(buf[pstart:pend])
271 for i in range(len(data)):
272 data[i] ^= mask[i % 4]
273 return data.tostring()
274
6a883409 275 @staticmethod
7d146027
JM
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:
f2d85676 292 header = pack('>BB', b1, payload_len)
fa74a6e6 293 elif payload_len > 125 and payload_len < 65536:
f2d85676 294 header = pack('>BBH', b1, 126, payload_len)
7d146027 295 elif payload_len >= 65536:
f2d85676 296 header = pack('>BBQ', b1, 127, payload_len)
7d146027 297
8c305c60 298 #print("Encoded: %s" % repr(header + buf))
7d146027 299
8c305c60 300 return header + buf, len(header), 0
6a883409
JM
301
302 @staticmethod
7d146027
JM
303 def decode_hybi(buf, base64=False):
304 """ Decode HyBi style WebSocket packets.
305 Returns:
306 {'fin' : 0_or_1,
307 'opcode' : number,
49578da4 308 'masked' : boolean,
8c305c60 309 'hlen' : header_bytes_number,
7d146027
JM
310 'length' : payload_bytes_number,
311 'payload' : decoded_buffer,
312 'left' : bytes_left_number,
313 'close_code' : number,
314 'close_reason' : string}
315 """
316
8c305c60
JM
317 f = {'fin' : 0,
318 'opcode' : 0,
49578da4 319 'masked' : False,
8c305c60
JM
320 'hlen' : 2,
321 'length' : 0,
322 'payload' : None,
323 'left' : 0,
c3900110
JM
324 'close_code' : 1000,
325 'close_reason' : ''}
7d146027
JM
326
327 blen = len(buf)
8c305c60 328 f['left'] = blen
7d146027 329
8c305c60
JM
330 if blen < f['hlen']:
331 return f # Incomplete frame header
7d146027 332
f2d85676 333 b1, b2 = unpack_from(">BB", buf)
8c305c60
JM
334 f['opcode'] = b1 & 0x0f
335 f['fin'] = (b1 & 0x80) >> 7
49578da4 336 f['masked'] = (b2 & 0x80) >> 7
7d146027 337
8c305c60 338 f['length'] = b2 & 0x7f
7d146027 339
8c305c60
JM
340 if f['length'] == 126:
341 f['hlen'] = 4
342 if blen < f['hlen']:
343 return f # Incomplete frame header
f2d85676 344 (f['length'],) = unpack_from('>xxH', buf)
8c305c60
JM
345 elif f['length'] == 127:
346 f['hlen'] = 10
347 if blen < f['hlen']:
348 return f # Incomplete frame header
f2d85676 349 (f['length'],) = unpack_from('>xxQ', buf)
7d146027 350
49578da4 351 full_len = f['hlen'] + f['masked'] * 4 + f['length']
7d146027
JM
352
353 if blen < full_len: # Incomplete frame
8c305c60 354 return f # Incomplete frame header
7d146027
JM
355
356 # Number of bytes that are part of the next frame(s)
8c305c60 357 f['left'] = blen - full_len
7d146027
JM
358
359 # Process 1 frame
49578da4 360 if f['masked']:
7d146027 361 # unmask payload
49578da4
JM
362 f['payload'] = WebSocketServer.unmask(buf, f['hlen'],
363 f['length'])
6a883409 364 else:
8c305c60 365 print("Unmasked frame: %s" % repr(buf))
49578da4 366 f['payload'] = buf[(f['hlen'] + f['masked'] * 4):full_len]
7d146027 367
8c305c60 368 if base64 and f['opcode'] in [1, 2]:
7d146027 369 try:
8c305c60 370 f['payload'] = b64decode(f['payload'])
7d146027 371 except:
8c305c60
JM
372 print("Exception while b64decoding buffer: %s" %
373 repr(buf))
7d146027
JM
374 raise
375
8c305c60
JM
376 if f['opcode'] == 0x08:
377 if f['length'] >= 2:
53dfab7f 378 f['close_code'] = unpack_from(">H", f['payload'])[0]
8c305c60
JM
379 if f['length'] > 3:
380 f['close_reason'] = f['payload'][2:]
7d146027 381
8c305c60 382 return f
7d146027
JM
383
384 @staticmethod
385 def encode_hixie(buf):
8c305c60 386 return s2b("\x00" + b2s(b64encode(buf)) + "\xff"), 1, 1
7d146027
JM
387
388 @staticmethod
389 def decode_hixie(buf):
8c305c60 390 end = buf.find(s2b('\xff'))
7d146027 391 return {'payload': b64decode(buf[1:end]),
8c305c60 392 'hlen': 1,
49578da4 393 'masked': False,
8c305c60 394 'length': end - 1,
7d146027
JM
395 'left': len(buf) - (end + 1)}
396
6a883409 397
6a883409
JM
398 @staticmethod
399 def gen_md5(keys):
7d146027 400 """ Generate hash value for WebSockets hixie-76. """
6a883409
JM
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
f2d85676 409 return b2s(md5(pack('>II8s',
8c305c60 410 int(num1), int(num2), key3)).digest())
6a883409 411
6a883409
JM
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:
8c305c60 425 print("% 3d: %s" % (self.handler_id, msg))
6a883409
JM
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 #
7d146027
JM
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
8c305c60
JM
443 tdelta = int(time.time()*1000) - self.start_time
444
7d146027
JM
445 if bufs:
446 for buf in bufs:
447 if self.version.startswith("hybi"):
448 if self.base64:
8c305c60
JM
449 encbuf, lenhead, lentail = self.encode_hybi(
450 buf, opcode=1, base64=True)
7d146027 451 else:
8c305c60
JM
452 encbuf, lenhead, lentail = self.encode_hybi(
453 buf, opcode=2, base64=False)
454
7d146027 455 else:
8c305c60
JM
456 encbuf, lenhead, lentail = self.encode_hixie(buf)
457
458 if self.rec:
459 self.rec.write("%s,\n" %
460 repr("{%s{" % tdelta
49578da4 461 + encbuf[lenhead:len(encbuf)-lentail]))
8c305c60
JM
462
463 self.send_parts.append(encbuf)
7d146027
JM
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 = []
8c305c60 488 tdelta = int(time.time()*1000) - self.start_time
7d146027
JM
489
490 buf = self.client.recv(self.buffer_size)
491 if len(buf) == 0:
c3900110 492 closed = {'code': 1000, 'reason': "Client closed abruptly"}
7d146027
JM
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)
8c305c60 504 #print("Received buf: %s, frame: %s" % (repr(buf), frame))
7d146027
JM
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
c3900110
JM
514 closed = {'code': frame['close_code'],
515 'reason': frame['close_reason']}
7d146027
JM
516 break
517
518 else:
1e508715 519 if buf[0:2] == s2b('\xff\x00'):
c3900110
JM
520 closed = {'code': 1000,
521 'reason': "Client sent orderly close frame"}
7d146027
JM
522 break
523
1e508715 524 elif buf[0:2] == s2b('\x00\xff'):
7d146027
JM
525 buf = buf[2:]
526 continue # No-op
527
8c305c60 528 elif buf.count(s2b('\xff')) == 0:
7d146027
JM
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
8c305c60
JM
538 if self.rec:
539 start = frame['hlen']
540 end = frame['hlen'] + frame['length']
49578da4
JM
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']]
8c305c60 547 self.rec.write("%s,\n" %
49578da4 548 repr("}%s}" % tdelta + recbuf))
8c305c60
JM
549
550
7d146027
JM
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
c3900110 560 def send_close(self, code=1000, reason=''):
7d146027
JM
561 """ Send a WebSocket orderly close frame. """
562
563 if self.version.startswith("hybi"):
c3900110 564 msg = pack(">H%ds" % len(reason), code, reason)
7d146027 565
3a39bf60 566 buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False)
7d146027
JM
567 self.client.send(buf)
568
569 elif self.version == "hixie-76":
8c305c60 570 buf = s2b('\xff\x00')
7d146027
JM
571 self.client.send(buf)
572
573 # No orderly close for 75
6a883409 574
204675c8
JM
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
6a883409
JM
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 """
6a883409 658 stype = ""
fc003a13 659 ready = select.select([sock], [], [], 3)[0]
204675c8
JM
660
661
ec2b6140
JM
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
6a883409 666 handshake = sock.recv(1024, socket.MSG_PEEK)
557efaf8 667 #self.msg("Handshake [%s]" % handshake)
6a883409
JM
668
669 if handshake == "":
670 raise self.EClose("ignoring empty handshake")
671
8c305c60 672 elif handshake.startswith(s2b("<policy-file-request/>")):
6a883409
JM
673 # Answer Flash policy request
674 handshake = sock.recv(1024)
8c305c60 675 sock.send(s2b(self.policy_response))
6a883409
JM
676 raise self.EClose("Sending flash policy response")
677
2fa565b3 678 elif handshake[0] in ("\x16", "\x80", 22, 128):
6a883409 679 # SSL wrap the connection
8c305c60
JM
680 if not ssl:
681 raise self.EClose("SSL connection but no 'ssl' module")
6a883409
JM
682 if not os.path.exists(self.cert):
683 raise self.EClose("SSL connection but '%s' not found"
684 % self.cert)
123e5e74 685 retsock = None
6a883409
JM
686 try:
687 retsock = ssl.wrap_socket(
688 sock,
689 server_side=True,
690 certfile=self.cert,
691 keyfile=self.key)
8c305c60
JM
692 except ssl.SSLError:
693 _, x, _ = sys.exc_info()
6a883409 694 if x.args[0] == ssl.SSL_ERROR_EOF:
0c4f4b59
JM
695 if len(x.args) > 1:
696 raise self.EClose(x.args[1])
697 else:
698 raise self.EClose("Got SSL_ERROR_EOF")
6a883409
JM
699 else:
700 raise
701
204675c8 702 self.scheme = "wss"
6a883409
JM
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
204675c8 710 self.scheme = "ws"
6a883409
JM
711 stype = "Plain non-SSL (ws://)"
712
8c305c60
JM
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("")
6a883409 725
204675c8 726 response = self.do_websocket_handshake(wsh.headers, wsh.path)
6a883409 727
7d146027
JM
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))
1e508715
JM
731 if self.path != '/':
732 self.msg("%s: Path: '%s'" % (address[0], self.path))
733
6a883409
JM
734
735 # Send server WebSockets handshake response
557efaf8 736 #self.msg("sending response [%s]" % response)
8c305c60 737 retsock.send(s2b(response))
6a883409
JM
738
739 # Return the WebSockets socket which may be SSL wrapped
740 return retsock
741
742
f2538f33
JM
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. """
ec2b6140
JM
752 #self.vmsg("Running poll()")
753 pass
754
8c305c60
JM
755 def fallback_SIGCHLD(self, sig, stack):
756 # Reap zombies when using os.fork() (python 2.4)
ec2b6140 757 self.vmsg("Got SIGCHLD, reaping zombies")
215ae8e5
JM
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
f2538f33 765
f2538f33
JM
766 def do_SIGINT(self, sig, stack):
767 self.msg("Got SIGINT, exiting")
768 sys.exit(0)
769
8c305c60
JM
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
204675c8 779 # handler process
8c305c60
JM
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+')
49578da4
JM
790 encoding = "binary"
791 if self.base64: encoding = "base64"
792 self.rec.write("var VNC_frame_encoding = '%s';\n"
793 % encoding)
8c305c60
JM
794 self.rec.write("var VNC_frame_data = [\n")
795
1e508715 796 self.ws_connection = True
8c305c60 797 self.new_client()
02d1f19b 798 except self.CClose:
c3900110
JM
799 # Close the client
800 _, exc, _ = sys.exc_info()
801 if self.client:
802 self.send_close(exc.args[0], exc.args[1])
8c305c60
JM
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:
49578da4 815 self.rec.write("'EOF'];\n")
8c305c60
JM
816 self.rec.close()
817
818 if self.client and self.client != startsock:
c3900110
JM
819 # Close the SSL wrapped socket
820 # Original socket closed by caller
8c305c60
JM
821 self.client.close()
822
823 def new_client(self):
6a883409 824 """ Do something with a WebSockets client connection. """
f2538f33 825 raise("WebSocketServer.new_client() must be overloaded")
6a883409
JM
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
f2538f33
JM
831 is a WebSockets client then call new_client() method (which must
832 be overridden) for each new client connection.
6a883409 833 """
204675c8 834 lsock = self.socket(self.listen_host, self.listen_port, False, self.prefer_ipv6)
6a883409 835
6a883409 836 if self.daemon:
7d146027 837 self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
6a883409 838
f2538f33
JM
839 self.started() # Some things need to happen after daemonizing
840
8c305c60 841 # Allow override of SIGINT
f2538f33 842 signal.signal(signal.SIGINT, self.do_SIGINT)
f2d85676 843 if not multiprocessing:
8c305c60
JM
844 # os.fork() (python 2.4) child reaper
845 signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD)
6a883409 846
204675c8 847 last_active_time = self.launch_time
6a883409
JM
848 while True:
849 try:
f2538f33 850 try:
7d146027
JM
851 self.client = None
852 startsock = None
66937e39 853 pid = err = 0
204675c8
JM
854 child_count = 0
855
856 if multiprocessing and self.idle_timeout:
857 child_count = len(multiprocessing.active_children())
66937e39 858
1e508715
JM
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
204675c8
JM
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
66937e39
JM
878 try:
879 self.poll()
880
8c305c60 881 ready = select.select([lsock], [], [], 1)[0]
66937e39
JM
882 if lsock in ready:
883 startsock, address = lsock.accept()
884 else:
885 continue
8c305c60
JM
886 except Exception:
887 _, exc, _ = sys.exc_info()
66937e39
JM
888 if hasattr(exc, 'errno'):
889 err = exc.errno
8c305c60
JM
890 elif hasattr(exc, 'args'):
891 err = exc.args[0]
66937e39
JM
892 else:
893 err = exc[0]
894 if err == errno.EINTR:
895 self.vmsg("Ignoring interrupted syscall")
896 continue
897 else:
898 raise
204675c8 899
1e508715
JM
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
f2d85676 907 elif multiprocessing:
8c305c60 908 self.vmsg('%s: new handler Process' % address[0])
f2d85676
JM
909 p = multiprocessing.Process(
910 target=self.top_new_client,
8c305c60
JM
911 args=(startsock, address))
912 p.start()
913 # child will not return
f2538f33 914 else:
8c305c60
JM
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")
66937e39 929 pass
8c305c60
JM
930 except SystemExit:
931 _, exc, _ = sys.exc_info()
932 print("In SystemExit")
933 break
934 except Exception:
935 _, exc, _ = sys.exc_info()
66937e39
JM
936 self.msg("handler exception: %s" % str(exc))
937 if self.verbose:
938 self.msg(traceback.format_exc())
f2538f33 939
6a883409 940 finally:
6a883409
JM
941 if startsock:
942 startsock.close()
943
705c54ed
JM
944 # Close listen port
945 self.vmsg("Closing socket listening at %s:%s"
946 % (self.listen_host, self.listen_port))
947 lsock.close()
948
a0315ab1 949
8c305c60
JM
950# HTTP handler with WebSocket upgrade support
951class WSRequestHandler(SimpleHTTPRequestHandler):
952 def __init__(self, req, addr, only_upgrade=False):
953 self.only_upgrade = only_upgrade # only allow upgrades
96bc3d30
JM
954 SimpleHTTPRequestHandler.__init__(self, req, addr, object())
955
8c305c60
JM
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)
96bc3d30
JM
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