]>
git.proxmox.com Git - mirror_novnc.git/blob - utils/websocket.py
4 Python WebSocket library with support for "wss://" encryption.
5 Copyright 2011 Joel Martin
6 Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3)
8 Supports following protocol versions:
9 - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-75
10 - http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
11 - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
13 You can make a cert/key with openssl using:
14 openssl req -new -x509 -days 365 -nodes -out self.pem -keyout self.pem
15 as taken from http://docs.python.org/dev/library/ssl.html#certificates
19 import os
, sys
, time
, errno
, signal
, socket
, struct
, traceback
, select
20 from cgi
import parse_qsl
21 from base64
import b64encode
, b64decode
23 # Imports that vary by python version
24 if sys
.hexversion
> 0x3000000:
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')
33 from cStringIO
import StringIO
34 from SimpleHTTPServer
import SimpleHTTPRequestHandler
35 from urlparse
import urlsplit
40 if sys
.hexversion
>= 0x2060000:
42 from multiprocessing
import Process
43 from hashlib
import md5
, sha1
48 from sha
import sha
as sha1
50 # Degraded functionality if these imports are missing
51 for mod
, sup
in [('numpy', 'HyBi protocol'),
52 ('ssl', 'TLS/SSL/wss'), ('resource', 'daemonizing')]:
54 globals()[mod
] = __import__(mod
)
57 print("WARNING: no '%s' module, %s support disabled" % (
61 class WebSocketServer(object):
63 WebSockets server class.
64 Must be sub-classed with new_client method definition.
69 server_handshake_hixie
= """HTTP/1.1 101 Web Socket Protocol Handshake\r
72 %sWebSocket-Origin: %s\r
73 %sWebSocket-Location: %s://%s%s\r
76 server_handshake_hybi
= """HTTP/1.1 101 Switching Protocols\r
79 Sec-WebSocket-Accept: %s\r
82 GUID
= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
84 policy_response
= """<cross-domain-policy><allow-access-from domain="*" to-ports="*" /></cross-domain-policy>\n"""
86 class EClose(Exception):
89 def __init__(self
, listen_host
='', listen_port
=None, source_is_ipv6
=False,
90 verbose
=False, cert
='', key
='', ssl_only
=None,
91 daemon
=False, record
='', web
=''):
94 self
.verbose
= verbose
95 self
.listen_host
= listen_host
96 self
.listen_port
= listen_port
97 self
.ssl_only
= ssl_only
101 # Make paths settings absolute
102 self
.cert
= os
.path
.abspath(cert
)
103 self
.key
= self
.web
= self
.record
= ''
105 self
.key
= os
.path
.abspath(key
)
107 self
.web
= os
.path
.abspath(web
)
109 self
.record
= os
.path
.abspath(record
)
115 if not ssl
and self
.ssl_only
:
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")
121 print("WebSocket server settings:")
122 print(" - Listen on %s:%s" % (
123 self
.listen_host
, self
.listen_port
))
124 print(" - Flash security policy server")
126 print(" - Web server. Web root: %s" % self
.web
)
128 if os
.path
.exists(self
.cert
):
129 print(" - SSL/TLS support")
131 print(" - Deny non-SSL/TLS connections")
133 print(" - No SSL/TLS support (no cert file)")
135 print(" - No SSL/TLS support (no 'ssl' module)")
137 print(" - Backgrounding (daemon)")
139 print(" - Recording to '%s.*'" % self
.record
)
142 # WebSocketServer static methods
146 def socket(host
, port
=None, connect
=False, prefer_ipv6
=False):
147 """ Resolve a host (and optional port) to an IPv4 or IPv6
148 address. Create a socket. Bind to it if listen is set,
149 otherwise connect to it. Return the socket.
154 if connect
and not port
:
155 raise Exception("Connect mode requires a port")
157 flags
= flags | socket
.AI_PASSIVE
158 addrs
= socket
.getaddrinfo(host
, port
, 0, socket
.SOCK_STREAM
,
159 socket
.IPPROTO_TCP
, flags
)
161 raise Exception("Could resolve host '%s'" % host
)
162 addrs
.sort(key
=lambda x
: x
[0])
165 sock
= socket
.socket(addrs
[0][0], addrs
[0][1])
167 sock
.connect(addrs
[0][4])
169 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
170 sock
.bind(addrs
[0][4])
175 def daemonize(keepfd
=None, chdir
='/'):
181 os
.setgid(os
.getgid()) # relinquish elevations
182 os
.setuid(os
.getuid()) # relinquish elevations
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
190 def terminate(a
,b
): os
._exit
(0)
191 signal
.signal(signal
.SIGTERM
, terminate
)
192 signal
.signal(signal
.SIGINT
, signal
.SIG_IGN
)
195 maxfd
= resource
.getrlimit(resource
.RLIMIT_NOFILE
)[1]
196 if maxfd
== resource
.RLIM_INFINITY
: maxfd
= 256
197 for fd
in reversed(range(maxfd
)):
202 _
, exc
, _
= sys
.exc_info()
203 if exc
.errno
!= errno
.EBADF
: raise
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())
211 def encode_hybi(buf
, opcode
, base64
=False):
212 """ Encode a HyBi style WebSocket frame.
215 0x1 - text frame (base64 encode buf)
216 0x2 - binary frame (use raw buf)
217 0x8 - connection close
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
)
233 #print("Encoded: %s" % repr(header + buf))
235 return header
+ buf
, len(header
), 0
238 def decode_hybi(buf
, base64
=False):
239 """ Decode HyBi style WebSocket packets.
243 'mask' : 32_bit_number,
244 'hlen' : header_bytes_number,
245 'length' : payload_bytes_number,
246 'payload' : decoded_buffer,
247 'left' : bytes_left_number,
248 'close_code' : number,
249 'close_reason' : string}
260 'close_reason' : None}
266 return f
# Incomplete frame header
268 b1
, b2
= struct
.unpack_from(">BB", buf
)
269 f
['opcode'] = b1
& 0x0f
270 f
['fin'] = (b1
& 0x80) >> 7
271 has_mask
= (b2
& 0x80) >> 7
273 f
['length'] = b2
& 0x7f
275 if f
['length'] == 126:
278 return f
# Incomplete frame header
279 (f
['length'],) = struct
.unpack_from('>xxH', buf
)
280 elif f
['length'] == 127:
283 return f
# Incomplete frame header
284 (f
['length'],) = struct
.unpack_from('>xxQ', buf
)
286 full_len
= f
['hlen'] + has_mask
* 4 + f
['length']
288 if blen
< full_len
: # Incomplete frame
289 return f
# Incomplete frame header
291 # Number of bytes that are part of the next frame(s)
292 f
['left'] = blen
- full_len
297 f
['mask'] = buf
[f
['hlen']:f
['hlen']+4]
300 mask
= numpy
.frombuffer(buf
, dtype
=numpy
.dtype('<u4'),
301 offset
=f
['hlen'], count
=1)
302 data
= numpy
.frombuffer(buf
, dtype
=numpy
.dtype('<u4'),
303 offset
=f
['hlen'] + 4, count
=int(f
['length'] / 4))
304 #b = numpy.bitwise_xor(data, mask).data
305 b
= numpy
.bitwise_xor(data
, mask
).tostring()
308 #print("Partial unmask")
309 mask
= numpy
.frombuffer(buf
, dtype
=numpy
.dtype('B'),
310 offset
=f
['hlen'], count
=(f
['length'] % 4))
311 data
= numpy
.frombuffer(buf
, dtype
=numpy
.dtype('B'),
312 offset
=full_len
- (f
['length'] % 4),
313 count
=(f
['length'] % 4))
314 c
= numpy
.bitwise_xor(data
, mask
).tostring()
317 print("Unmasked frame: %s" % repr(buf
))
318 f
['payload'] = buf
[(f
['hlen'] + has_mask
* 4):full_len
]
320 if base64
and f
['opcode'] in [1, 2]:
322 f
['payload'] = b64decode(f
['payload'])
324 print("Exception while b64decoding buffer: %s" %
328 if f
['opcode'] == 0x08:
330 f
['close_code'] = struct
.unpack_from(">H", f
['payload'])
332 f
['close_reason'] = f
['payload'][2:]
337 def encode_hixie(buf
):
338 return s2b("\x00" + b2s(b64encode(buf
)) + "\xff"), 1, 1
341 def decode_hixie(buf
):
342 end
= buf
.find(s2b('\xff'))
343 return {'payload': b64decode(buf
[1:end
]),
346 'left': len(buf
) - (end
+ 1)}
351 """ Generate hash value for WebSockets hixie-76. """
352 key1
= keys
['Sec-WebSocket-Key1']
353 key2
= keys
['Sec-WebSocket-Key2']
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
360 return b2s(md5(struct
.pack('>II8s',
361 int(num1
), int(num2
), key3
)).digest())
364 # WebSocketServer logging/output functions
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
)
374 """ Output message with handler_id prefix. """
376 print("% 3d: %s" % (self
.handler_id
, msg
))
379 """ Same as msg() but only if verbose. """
384 # Main WebSocketServer methods
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
394 tdelta
= int(time
.time()*1000) - self
.start_time
398 if self
.version
.startswith("hybi"):
400 encbuf
, lenhead
, lentail
= self
.encode_hybi(
401 buf
, opcode
=1, base64
=True)
403 encbuf
, lenhead
, lentail
= self
.encode_hybi(
404 buf
, opcode
=2, base64
=False)
407 encbuf
, lenhead
, lentail
= self
.encode_hixie(buf
)
410 self
.rec
.write("%s,\n" %
412 + encbuf
[lenhead
:-lentail
]))
414 self
.send_parts
.append(encbuf
)
416 while self
.send_parts
:
417 # Send pending frames
418 buf
= self
.send_parts
.pop(0)
419 sent
= self
.client
.send(buf
)
425 self
.send_parts
.insert(0, buf
[sent
:])
428 return len(self
.send_parts
)
430 def recv_frames(self
):
431 """ Receive and decode WebSocket frames.
434 (bufs_list, closed_string)
439 tdelta
= int(time
.time()*1000) - self
.start_time
441 buf
= self
.client
.recv(self
.buffer_size
)
443 closed
= "Client closed abruptly"
447 # Add partially received frames to current read buffer
448 buf
= self
.recv_part
+ buf
449 self
.recv_part
= None
452 if self
.version
.startswith("hybi"):
454 frame
= self
.decode_hybi(buf
, base64
=self
.base64
)
455 #print("Received buf: %s, frame: %s" % (repr(buf), frame))
457 if frame
['payload'] == None:
458 # Incomplete/partial frame
460 if frame
['left'] > 0:
461 self
.recv_part
= buf
[-frame
['left']:]
464 if frame
['opcode'] == 0x8: # connection close
465 closed
= "Client closed, reason: %s - %s" % (
467 frame
['close_reason'])
471 if buf
[0:2] == '\xff\x00':
472 closed
= "Client sent orderly close frame"
475 elif buf
[0:2] == '\x00\xff':
479 elif buf
.count(s2b('\xff')) == 0:
485 frame
= self
.decode_hixie(buf
)
490 start
= frame
['hlen']
491 end
= frame
['hlen'] + frame
['length']
492 self
.rec
.write("%s,\n" %
493 repr("}%s}" % tdelta
+ buf
[start
:end
]))
496 bufs
.append(frame
['payload'])
499 buf
= buf
[-frame
['left']:]
505 def send_close(self
, code
=None, reason
=''):
506 """ Send a WebSocket orderly close frame. """
508 if self
.version
.startswith("hybi"):
511 msg
= struct
.pack(">H%ds" % (len(reason
)), code
)
513 buf
, h
, t
= self
.encode_hybi(msg
, opcode
=0x08, base64
=False)
514 self
.client
.send(buf
)
516 elif self
.version
== "hixie-76":
517 buf
= s2b('\xff\x00')
518 self
.client
.send(buf
)
520 # No orderly close for 75
522 def do_handshake(self
, sock
, address
):
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
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
534 - Assume we have a WebSockets connection, parse the client
536 - Send a WebSockets handshake server response.
537 - Return the socket for this WebSocket client.
542 ready
= select
.select([sock
], [], [], 3)[0]
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
547 handshake
= sock
.recv(1024, socket
.MSG_PEEK
)
548 #self.msg("Handshake [%s]" % handshake)
551 raise self
.EClose("ignoring empty handshake")
553 elif handshake
.startswith(s2b("<policy-file-request/>")):
554 # Answer Flash policy request
555 handshake
= sock
.recv(1024)
556 sock
.send(s2b(self
.policy_response
))
557 raise self
.EClose("Sending flash policy response")
559 elif handshake
[0] in ("\x16", "\x80"):
560 # SSL wrap the connection
562 raise self
.EClose("SSL connection but no 'ssl' module")
563 if not os
.path
.exists(self
.cert
):
564 raise self
.EClose("SSL connection but '%s' not found"
568 retsock
= ssl
.wrap_socket(
574 _
, x
, _
= sys
.exc_info()
575 if x
.args
[0] == ssl
.SSL_ERROR_EOF
:
576 raise self
.EClose("")
581 stype
= "SSL/TLS (wss://)"
584 raise self
.EClose("non-SSL connection received but disallowed")
589 stype
= "Plain non-SSL (ws://)"
591 wsh
= WSRequestHandler(retsock
, address
, not self
.web
)
592 if wsh
.last_code
== 101:
593 # Continue on to handle WebSocket upgrade
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
)
600 raise self
.EClose(wsh
.last_message
)
602 raise self
.EClose("")
604 h
= self
.headers
= wsh
.headers
605 path
= self
.path
= wsh
.path
607 prot
= 'WebSocket-Protocol'
608 protocols
= h
.get('Sec-'+prot
, h
.get(prot
, '')).split(',')
610 ver
= h
.get('Sec-WebSocket-Version')
612 # HyBi/IETF version of the protocol
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")
617 # HyBi-07 report version 7
618 # HyBi-08 - HyBi-12 report version 8
619 # HyBi-13 reports version 13
620 if ver
in ['7', '8', '13']:
621 self
.version
= "hybi-%02d" % int(ver
)
623 raise self
.EClose('Unsupported protocol version %s' % ver
)
625 key
= h
['Sec-WebSocket-Key']
627 # Choose binary if client supports it
628 if 'binary' in protocols
:
630 elif 'base64' in protocols
:
633 raise self
.EClose("Client must support 'binary' or 'base64' protocol")
635 # Generate the hash value for the accept header
636 accept
= b64encode(sha1(s2b(key
+ self
.GUID
)).digest())
638 response
= self
.server_handshake_hybi
% b2s(accept
)
640 response
+= "Sec-WebSocket-Protocol: base64\r\n"
642 response
+= "Sec-WebSocket-Protocol: binary\r\n"
646 # Hixie version of the protocol (75 or 76)
649 trailer
= self
.gen_md5(h
)
651 self
.version
= "hixie-76"
655 self
.version
= "hixie-75"
657 # We only support base64 in Hixie era
660 response
= self
.server_handshake_hixie
% (pre
,
661 h
['Origin'], pre
, scheme
, h
['Host'], path
)
663 if 'base64' in protocols
:
664 response
+= "%sWebSocket-Protocol: base64\r\n" % pre
666 self
.msg("Warning: client does not report 'base64' protocol support")
667 response
+= "\r\n" + trailer
669 self
.msg("%s: %s WebSocket connection" % (address
[0], stype
))
670 self
.msg("%s: Version %s, base64: '%s'" % (address
[0],
671 self
.version
, self
.base64
))
673 # Send server WebSockets handshake response
674 #self.msg("sending response [%s]" % response)
675 retsock
.send(s2b(response
))
677 # Return the WebSockets socket which may be SSL wrapped
682 # Events that can/should be overridden in sub-classes
685 """ Called after WebSockets startup """
686 self
.vmsg("WebSockets server started")
689 """ Run periodically while waiting for connections. """
690 #self.vmsg("Running poll()")
693 def fallback_SIGCHLD(self
, sig
, stack
):
694 # Reap zombies when using os.fork() (python 2.4)
695 self
.vmsg("Got SIGCHLD, reaping zombies")
697 result
= os
.waitpid(-1, os
.WNOHANG
)
699 self
.vmsg("Reaped child process %s" % result
[0])
700 result
= os
.waitpid(-1, os
.WNOHANG
)
704 def do_SIGINT(self
, sig
, stack
):
705 self
.msg("Got SIGINT, exiting")
708 def top_new_client(self
, startsock
, address
):
709 """ Do something with a WebSockets client connection. """
710 # Initialize per client settings
712 self
.recv_part
= None
715 self
.start_time
= int(time
.time()*1000)
720 self
.client
= self
.do_handshake(startsock
, address
)
723 # Record raw frame data as JavaScript array
724 fname
= "%s.%s" % (self
.record
,
726 self
.msg("opening record file: %s" % fname
)
727 self
.rec
= open(fname
, 'w+')
728 self
.rec
.write("var VNC_frame_data = [\n")
732 _
, exc
, _
= sys
.exc_info()
733 # Connection was not a WebSockets connection
735 self
.msg("%s: %s" % (address
[0], exc
.args
[0]))
737 _
, exc
, _
= sys
.exc_info()
738 self
.msg("handler exception: %s" % str(exc
))
740 self
.msg(traceback
.format_exc())
743 self
.rec
.write("'EOF']\n")
746 if self
.client
and self
.client
!= startsock
:
749 def new_client(self
):
750 """ Do something with a WebSockets client connection. """
751 raise("WebSocketServer.new_client() must be overloaded")
753 def start_server(self
):
755 Daemonize if requested. Listen for for connections. Run
756 do_handshake() method for each connection. If the connection
757 is a WebSockets client then call new_client() method (which must
758 be overridden) for each new client connection.
760 lsock
= self
.socket(self
.listen_host
, self
.listen_port
)
763 self
.daemonize(keepfd
=lsock
.fileno(), chdir
=self
.web
)
765 self
.started() # Some things need to happen after daemonizing
767 # Allow override of SIGINT
768 signal
.signal(signal
.SIGINT
, self
.do_SIGINT
)
770 # os.fork() (python 2.4) child reaper
771 signal
.signal(signal
.SIGCHLD
, self
.fallback_SIGCHLD
)
783 ready
= select
.select([lsock
], [], [], 1)[0]
785 startsock
, address
= lsock
.accept()
789 _
, exc
, _
= sys
.exc_info()
790 if hasattr(exc
, 'errno'):
792 elif hasattr(exc
, 'args'):
796 if err
== errno
.EINTR
:
797 self
.vmsg("Ignoring interrupted syscall")
803 self
.vmsg('%s: new handler Process' % address
[0])
804 p
= Process(target
=self
.top_new_client
,
805 args
=(startsock
, address
))
807 # child will not return
810 self
.vmsg('%s: forking handler' % address
[0])
813 # child handler process
814 self
.top_new_client(startsock
, address
)
815 break # child process exits
820 except KeyboardInterrupt:
821 _
, exc
, _
= sys
.exc_info()
822 print("In KeyboardInterrupt")
825 _
, exc
, _
= sys
.exc_info()
826 print("In SystemExit")
829 _
, exc
, _
= sys
.exc_info()
830 self
.msg("handler exception: %s" % str(exc
))
832 self
.msg(traceback
.format_exc())
839 # HTTP handler with WebSocket upgrade support
840 class WSRequestHandler(SimpleHTTPRequestHandler
):
841 def __init__(self
, req
, addr
, only_upgrade
=False):
842 self
.only_upgrade
= only_upgrade
# only allow upgrades
843 SimpleHTTPRequestHandler
.__init
__(self
, req
, addr
, object())
846 if (self
.headers
.get('upgrade') and
847 self
.headers
.get('upgrade').lower() == 'websocket'):
849 if (self
.headers
.get('sec-websocket-key1') or
850 self
.headers
.get('websocket-key1')):
851 # For Hixie-76 read out the key hash
852 self
.headers
.__setitem
__('key3', self
.rfile
.read(8))
854 # Just indicate that an WebSocket upgrade is needed
856 self
.last_message
= "101 Switching Protocols"
857 elif self
.only_upgrade
:
858 # Normal web request responses are disabled
860 self
.last_message
= "405 Method Not Allowed"
862 SimpleHTTPRequestHandler
.do_GET(self
)
864 def send_response(self
, code
, message
=None):
865 # Save the status code
866 self
.last_code
= code
867 SimpleHTTPRequestHandler
.send_response(self
, code
, message
)
869 def log_message(self
, f
, *args
):
870 # Save instead of printing
871 self
.last_message
= f
% args