|
@@ -18,7 +18,6 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates
|
|
|
|
|
|
import os, sys, time, errno, signal, socket, traceback, select
|
|
|
import array, struct
|
|
|
-from cgi import parse_qsl
|
|
|
from base64 import b64encode, b64decode
|
|
|
|
|
|
# Imports that vary by python version
|
|
@@ -36,8 +35,6 @@ try: from io import StringIO
|
|
|
except: from cStringIO import StringIO
|
|
|
try: from http.server import SimpleHTTPRequestHandler
|
|
|
except: from SimpleHTTPServer import SimpleHTTPRequestHandler
|
|
|
-try: from urllib.parse import urlsplit
|
|
|
-except: from urlparse import urlsplit
|
|
|
|
|
|
# python 2.6 differences
|
|
|
try: from hashlib import md5, sha1
|
|
@@ -75,6 +72,7 @@ class WebSocketServer(object):
|
|
|
|
|
|
buffer_size = 65536
|
|
|
|
|
|
+
|
|
|
server_handshake_hixie = """HTTP/1.1 101 Web Socket Protocol Handshake\r
|
|
|
Upgrade: WebSocket\r
|
|
|
Connection: Upgrade\r
|
|
@@ -103,17 +101,19 @@ Sec-WebSocket-Accept: %s\r
|
|
|
def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False,
|
|
|
verbose=False, cert='', key='', ssl_only=None,
|
|
|
daemon=False, record='', web='',
|
|
|
- run_once=False, timeout=0):
|
|
|
+ run_once=False, timeout=0, idle_timeout=0):
|
|
|
|
|
|
# settings
|
|
|
self.verbose = verbose
|
|
|
self.listen_host = listen_host
|
|
|
self.listen_port = listen_port
|
|
|
+ self.prefer_ipv6 = source_is_ipv6
|
|
|
self.ssl_only = ssl_only
|
|
|
self.daemon = daemon
|
|
|
self.run_once = run_once
|
|
|
self.timeout = timeout
|
|
|
-
|
|
|
+ self.idle_timeout = idle_timeout
|
|
|
+
|
|
|
self.launch_time = time.time()
|
|
|
self.ws_connection = False
|
|
|
self.handler_id = 1
|
|
@@ -163,7 +163,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
#
|
|
|
|
|
|
@staticmethod
|
|
|
- def socket(host, port=None, connect=False, prefer_ipv6=False):
|
|
|
+ def socket(host, port=None, connect=False, prefer_ipv6=False, unix_socket=None, use_ssl=False):
|
|
|
""" Resolve a host (and optional port) to an IPv4 or IPv6
|
|
|
address. Create a socket. Bind to it if listen is set,
|
|
|
otherwise connect to it. Return the socket.
|
|
@@ -171,24 +171,36 @@ Sec-WebSocket-Accept: %s\r
|
|
|
flags = 0
|
|
|
if host == '':
|
|
|
host = None
|
|
|
- if connect and not port:
|
|
|
+ if connect and not (port or unix_socket):
|
|
|
raise Exception("Connect mode requires a port")
|
|
|
+ if use_ssl and not ssl:
|
|
|
+ raise Exception("SSL socket requested but Python SSL module not loaded.");
|
|
|
+ if not connect and use_ssl:
|
|
|
+ raise Exception("SSL only supported in connect mode (for now)")
|
|
|
if not connect:
|
|
|
flags = flags | socket.AI_PASSIVE
|
|
|
- addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
|
|
|
- socket.IPPROTO_TCP, flags)
|
|
|
- if not addrs:
|
|
|
- raise Exception("Could resolve host '%s'" % host)
|
|
|
- addrs.sort(key=lambda x: x[0])
|
|
|
- if prefer_ipv6:
|
|
|
- addrs.reverse()
|
|
|
- sock = socket.socket(addrs[0][0], addrs[0][1])
|
|
|
- if connect:
|
|
|
- sock.connect(addrs[0][4])
|
|
|
- else:
|
|
|
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
- sock.bind(addrs[0][4])
|
|
|
- sock.listen(100)
|
|
|
+
|
|
|
+ if not unix_socket:
|
|
|
+ addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM,
|
|
|
+ socket.IPPROTO_TCP, flags)
|
|
|
+ if not addrs:
|
|
|
+ raise Exception("Could not resolve host '%s'" % host)
|
|
|
+ addrs.sort(key=lambda x: x[0])
|
|
|
+ if prefer_ipv6:
|
|
|
+ addrs.reverse()
|
|
|
+ sock = socket.socket(addrs[0][0], addrs[0][1])
|
|
|
+ if connect:
|
|
|
+ sock.connect(addrs[0][4])
|
|
|
+ if use_ssl:
|
|
|
+ sock = ssl.wrap_socket(sock)
|
|
|
+ else:
|
|
|
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
|
+ sock.bind(addrs[0][4])
|
|
|
+ sock.listen(100)
|
|
|
+ else:
|
|
|
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
|
+ sock.connect(unix_socket)
|
|
|
+
|
|
|
return sock
|
|
|
|
|
|
@staticmethod
|
|
@@ -552,6 +564,72 @@ Sec-WebSocket-Accept: %s\r
|
|
|
|
|
|
# No orderly close for 75
|
|
|
|
|
|
+ def do_websocket_handshake(self, headers, path):
|
|
|
+ h = self.headers = headers
|
|
|
+ self.path = path
|
|
|
+
|
|
|
+ prot = 'WebSocket-Protocol'
|
|
|
+ protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
|
|
|
+
|
|
|
+ ver = h.get('Sec-WebSocket-Version')
|
|
|
+ if ver:
|
|
|
+ # HyBi/IETF version of the protocol
|
|
|
+
|
|
|
+ # HyBi-07 report version 7
|
|
|
+ # HyBi-08 - HyBi-12 report version 8
|
|
|
+ # HyBi-13 reports version 13
|
|
|
+ if ver in ['7', '8', '13']:
|
|
|
+ self.version = "hybi-%02d" % int(ver)
|
|
|
+ else:
|
|
|
+ raise self.EClose('Unsupported protocol version %s' % ver)
|
|
|
+
|
|
|
+ key = h['Sec-WebSocket-Key']
|
|
|
+
|
|
|
+ # Choose binary if client supports it
|
|
|
+ if 'binary' in protocols:
|
|
|
+ self.base64 = False
|
|
|
+ elif 'base64' in protocols:
|
|
|
+ self.base64 = True
|
|
|
+ else:
|
|
|
+ raise self.EClose("Client must support 'binary' or 'base64' protocol")
|
|
|
+
|
|
|
+ # Generate the hash value for the accept header
|
|
|
+ accept = b64encode(sha1(s2b(key + self.GUID)).digest())
|
|
|
+
|
|
|
+ response = self.server_handshake_hybi % b2s(accept)
|
|
|
+ if self.base64:
|
|
|
+ response += "Sec-WebSocket-Protocol: base64\r\n"
|
|
|
+ else:
|
|
|
+ response += "Sec-WebSocket-Protocol: binary\r\n"
|
|
|
+ response += "\r\n"
|
|
|
+
|
|
|
+ else:
|
|
|
+ # Hixie version of the protocol (75 or 76)
|
|
|
+
|
|
|
+ if h.get('key3'):
|
|
|
+ trailer = self.gen_md5(h)
|
|
|
+ pre = "Sec-"
|
|
|
+ self.version = "hixie-76"
|
|
|
+ else:
|
|
|
+ trailer = ""
|
|
|
+ pre = ""
|
|
|
+ self.version = "hixie-75"
|
|
|
+
|
|
|
+ # We only support base64 in Hixie era
|
|
|
+ self.base64 = True
|
|
|
+
|
|
|
+ response = self.server_handshake_hixie % (pre,
|
|
|
+ h['Origin'], pre, self.scheme, h['Host'], path)
|
|
|
+
|
|
|
+ if 'base64' in protocols:
|
|
|
+ response += "%sWebSocket-Protocol: base64\r\n" % pre
|
|
|
+ else:
|
|
|
+ self.msg("Warning: client does not report 'base64' protocol support")
|
|
|
+ response += "\r\n" + trailer
|
|
|
+
|
|
|
+ return response
|
|
|
+
|
|
|
+
|
|
|
def do_handshake(self, sock, address):
|
|
|
"""
|
|
|
do_handshake does the following:
|
|
@@ -569,10 +647,10 @@ Sec-WebSocket-Accept: %s\r
|
|
|
- Send a WebSockets handshake server response.
|
|
|
- Return the socket for this WebSocket client.
|
|
|
"""
|
|
|
-
|
|
|
stype = ""
|
|
|
-
|
|
|
ready = select.select([sock], [], [], 3)[0]
|
|
|
+
|
|
|
+
|
|
|
if not ready:
|
|
|
raise self.EClose("ignoring socket not ready")
|
|
|
# Peek, but do not read the data so that we have a opportunity
|
|
@@ -613,7 +691,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
else:
|
|
|
raise
|
|
|
|
|
|
- scheme = "wss"
|
|
|
+ self.scheme = "wss"
|
|
|
stype = "SSL/TLS (wss://)"
|
|
|
|
|
|
elif self.ssl_only:
|
|
@@ -621,7 +699,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
|
|
|
else:
|
|
|
retsock = sock
|
|
|
- scheme = "ws"
|
|
|
+ self.scheme = "ws"
|
|
|
stype = "Plain non-SSL (ws://)"
|
|
|
|
|
|
wsh = WSRequestHandler(retsock, address, not self.web)
|
|
@@ -637,67 +715,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
else:
|
|
|
raise self.EClose("")
|
|
|
|
|
|
- h = self.headers = wsh.headers
|
|
|
- path = self.path = wsh.path
|
|
|
-
|
|
|
- prot = 'WebSocket-Protocol'
|
|
|
- protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',')
|
|
|
-
|
|
|
- ver = h.get('Sec-WebSocket-Version')
|
|
|
- if ver:
|
|
|
- # HyBi/IETF version of the protocol
|
|
|
-
|
|
|
- # HyBi-07 report version 7
|
|
|
- # HyBi-08 - HyBi-12 report version 8
|
|
|
- # HyBi-13 reports version 13
|
|
|
- if ver in ['7', '8', '13']:
|
|
|
- self.version = "hybi-%02d" % int(ver)
|
|
|
- else:
|
|
|
- raise self.EClose('Unsupported protocol version %s' % ver)
|
|
|
-
|
|
|
- key = h['Sec-WebSocket-Key']
|
|
|
-
|
|
|
- # Choose binary if client supports it
|
|
|
- if 'binary' in protocols:
|
|
|
- self.base64 = False
|
|
|
- elif 'base64' in protocols:
|
|
|
- self.base64 = True
|
|
|
- else:
|
|
|
- raise self.EClose("Client must support 'binary' or 'base64' protocol")
|
|
|
-
|
|
|
- # Generate the hash value for the accept header
|
|
|
- accept = b64encode(sha1(s2b(key + self.GUID)).digest())
|
|
|
-
|
|
|
- response = self.server_handshake_hybi % b2s(accept)
|
|
|
- if self.base64:
|
|
|
- response += "Sec-WebSocket-Protocol: base64\r\n"
|
|
|
- else:
|
|
|
- response += "Sec-WebSocket-Protocol: binary\r\n"
|
|
|
- response += "\r\n"
|
|
|
-
|
|
|
- else:
|
|
|
- # Hixie version of the protocol (75 or 76)
|
|
|
-
|
|
|
- if h.get('key3'):
|
|
|
- trailer = self.gen_md5(h)
|
|
|
- pre = "Sec-"
|
|
|
- self.version = "hixie-76"
|
|
|
- else:
|
|
|
- trailer = ""
|
|
|
- pre = ""
|
|
|
- self.version = "hixie-75"
|
|
|
-
|
|
|
- # We only support base64 in Hixie era
|
|
|
- self.base64 = True
|
|
|
-
|
|
|
- response = self.server_handshake_hixie % (pre,
|
|
|
- h['Origin'], pre, scheme, h['Host'], path)
|
|
|
-
|
|
|
- if 'base64' in protocols:
|
|
|
- response += "%sWebSocket-Protocol: base64\r\n" % pre
|
|
|
- else:
|
|
|
- self.msg("Warning: client does not report 'base64' protocol support")
|
|
|
- response += "\r\n" + trailer
|
|
|
+ response = self.do_websocket_handshake(wsh.headers, wsh.path)
|
|
|
|
|
|
self.msg("%s: %s WebSocket connection" % (address[0], stype))
|
|
|
self.msg("%s: Version %s, base64: '%s'" % (address[0],
|
|
@@ -750,7 +768,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
self.rec = None
|
|
|
self.start_time = int(time.time()*1000)
|
|
|
|
|
|
- # handler process
|
|
|
+ # handler process
|
|
|
try:
|
|
|
try:
|
|
|
self.client = self.do_handshake(startsock, address)
|
|
@@ -801,7 +819,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
is a WebSockets client then call new_client() method (which must
|
|
|
be overridden) for each new client connection.
|
|
|
"""
|
|
|
- lsock = self.socket(self.listen_host, self.listen_port)
|
|
|
+ lsock = self.socket(self.listen_host, self.listen_port, False, self.prefer_ipv6)
|
|
|
|
|
|
if self.daemon:
|
|
|
self.daemonize(keepfd=lsock.fileno(), chdir=self.web)
|
|
@@ -814,12 +832,17 @@ Sec-WebSocket-Accept: %s\r
|
|
|
# os.fork() (python 2.4) child reaper
|
|
|
signal.signal(signal.SIGCHLD, self.fallback_SIGCHLD)
|
|
|
|
|
|
+ last_active_time = self.launch_time
|
|
|
while True:
|
|
|
try:
|
|
|
try:
|
|
|
self.client = None
|
|
|
startsock = None
|
|
|
pid = err = 0
|
|
|
+ child_count = 0
|
|
|
+
|
|
|
+ if multiprocessing and self.idle_timeout:
|
|
|
+ child_count = len(multiprocessing.active_children())
|
|
|
|
|
|
time_elapsed = time.time() - self.launch_time
|
|
|
if self.timeout and time_elapsed > self.timeout:
|
|
@@ -827,6 +850,19 @@ Sec-WebSocket-Accept: %s\r
|
|
|
% self.timeout)
|
|
|
break
|
|
|
|
|
|
+ if self.idle_timeout:
|
|
|
+ idle_time = 0
|
|
|
+ if child_count == 0:
|
|
|
+ idle_time = time.time() - last_active_time
|
|
|
+ else:
|
|
|
+ idle_time = 0
|
|
|
+ last_active_time = time.time()
|
|
|
+
|
|
|
+ if idle_time > self.idle_timeout and child_count == 0:
|
|
|
+ self.msg('listener exit due to --idle-timeout %s'
|
|
|
+ % self.idle_timeout)
|
|
|
+ break
|
|
|
+
|
|
|
try:
|
|
|
self.poll()
|
|
|
|
|
@@ -848,7 +884,7 @@ Sec-WebSocket-Accept: %s\r
|
|
|
continue
|
|
|
else:
|
|
|
raise
|
|
|
-
|
|
|
+
|
|
|
if self.run_once:
|
|
|
# Run in same process if run_once
|
|
|
self.top_new_client(startsock, address)
|
|
@@ -927,4 +963,3 @@ class WSRequestHandler(SimpleHTTPRequestHandler):
|
|
|
def log_message(self, f, *args):
|
|
|
# Save instead of printing
|
|
|
self.last_message = f % args
|
|
|
-
|