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