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