]>
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 | ||
8c305c60 JM |
19 | import os, sys, time, errno, signal, socket, struct, traceback, select |
20 | from cgi import parse_qsl | |
95ef30a1 | 21 | from base64 import b64encode, b64decode |
8c305c60 JM |
22 | |
23 | # Imports that vary by python version | |
24 | if 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') | |
31 | else: | |
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 | ||
40 | if sys.hexversion >= 0x2060000: | |
41 | # python >= 2.6 | |
42 | from multiprocessing import Process | |
7d146027 | 43 | from hashlib import md5, sha1 |
8c305c60 JM |
44 | else: |
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 | |
51 | for 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 | 61 | class 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 |
70 | Upgrade: WebSocket\r |
71 | Connection: 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 | |
77 | Upgrade: websocket\r | |
78 | Connection: Upgrade\r | |
79 | Sec-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 |
839 | class 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 |