]>
Commit | Line | Data |
---|---|---|
e9155818 | 1 | #!/usr/bin/env python |
95ef30a1 JM |
2 | |
3 | ''' | |
4 | Python WebSocket library with support for "wss://" encryption. | |
8c305c60 | 5 | Copyright 2011 Joel Martin |
31407abc | 6 | Licensed under LGPL version 3 (see docs/LICENSE.LGPL-3) |
95ef30a1 | 7 | |
7d146027 JM |
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 | |
48f26d79 | 11 | - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10 |
7d146027 | 12 | |
95ef30a1 JM |
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 | |
16 | ||
17 | ''' | |
18 | ||
1e508715 | 19 | import os, sys, time, errno, signal, socket, traceback, select |
f2d85676 | 20 | import array, struct |
95ef30a1 | 21 | from base64 import b64encode, b64decode |
8c305c60 JM |
22 | |
23 | # Imports that vary by python version | |
f2d85676 JM |
24 | |
25 | # python 3.0 differences | |
8c305c60 | 26 | if 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 | 30 | else: |
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 |
34 | try: from io import StringIO |
35 | except: from cStringIO import StringIO | |
36 | try: from http.server import SimpleHTTPRequestHandler | |
37 | except: from SimpleHTTPServer import SimpleHTTPRequestHandler | |
f2d85676 JM |
38 | |
39 | # python 2.6 differences | |
40 | try: from hashlib import md5, sha1 | |
41 | except: from md5 import md5; from sha import sha as sha1 | |
42 | ||
43 | # python 2.5 differences | |
44 | try: | |
45 | from struct import pack, unpack_from | |
46 | except: | |
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 |
53 | for 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 |
62 | if multiprocessing and sys.platform == 'win32': |
63 | # make sockets pickle-able/inheritable | |
64 | import multiprocessing.reduction | |
8c305c60 | 65 | |
95ef30a1 | 66 | |
66937e39 | 67 | class 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 |
77 | Upgrade: WebSocket\r |
78 | Connection: 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 | |
84 | Upgrade: websocket\r | |
85 | Connection: Upgrade\r | |
86 | Sec-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 |
951 | class 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 |