From b9e1295f7afa34a4c69618609e053af154e477d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Thu, 14 Mar 2013 15:23:44 +0100 Subject: [PATCH 01/16] Prepare for solving https://github.com/kanaka/websockify/issues/71: Rename self.client to self.request, since this is what standard SocketServer request handlers are using. --- websockify/websocket.py | 16 ++++++++-------- websockify/websocketproxy.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 7adfca8..4f298bc 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -424,7 +424,7 @@ Sec-WebSocket-Accept: %s\r while self.send_parts: # Send pending frames buf = self.send_parts.pop(0) - sent = self.client.send(buf) + sent = self.request.send(buf) if sent == len(buf): self.traffic("<") @@ -446,7 +446,7 @@ Sec-WebSocket-Accept: %s\r bufs = [] tdelta = int(time.time()*1000) - self.start_time - buf = self.client.recv(self.buffer_size) + buf = self.request.recv(self.buffer_size) if len(buf) == 0: closed = {'code': 1000, 'reason': "Client closed abruptly"} return bufs, closed @@ -501,7 +501,7 @@ Sec-WebSocket-Accept: %s\r msg = pack(">H%ds" % len(reason), code, reason) buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False) - self.client.send(buf) + self.request.send(buf) def do_websocket_handshake(self, headers, path): h = self.headers = headers @@ -689,7 +689,7 @@ Sec-WebSocket-Accept: %s\r # handler process try: try: - self.client = self.do_handshake(startsock, address) + self.request = self.do_handshake(startsock, address) if self.record: # Record raw frame data as JavaScript array @@ -708,7 +708,7 @@ Sec-WebSocket-Accept: %s\r except self.CClose: # Close the client _, exc, _ = sys.exc_info() - if self.client: + if self.request: self.send_close(exc.args[0], exc.args[1]) except self.EClose: _, exc, _ = sys.exc_info() @@ -725,10 +725,10 @@ Sec-WebSocket-Accept: %s\r self.rec.write("'EOF'];\n") self.rec.close() - if self.client and self.client != startsock: + if self.request and self.request != startsock: # Close the SSL wrapped socket # Original socket closed by caller - self.client.close() + self.request.close() def new_client(self): """ Do something with a WebSockets client connection. """ @@ -758,7 +758,7 @@ Sec-WebSocket-Accept: %s\r while True: try: try: - self.client = None + self.request = None startsock = None pid = err = 0 child_count = 0 diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 1154d92..52b46b4 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -242,23 +242,23 @@ Traffic Legend: cqueue = [] c_pend = 0 tqueue = [] - rlist = [self.client, target] + rlist = [self.request, target] while True: wlist = [] if tqueue: wlist.append(target) - if cqueue or c_pend: wlist.append(self.client) + if cqueue or c_pend: wlist.append(self.request) ins, outs, excepts = select(rlist, wlist, [], 1) if excepts: raise Exception("Socket exception") - if self.client in outs: + if self.request in outs: # Send queued target data to the client c_pend = self.send_frames(cqueue) cqueue = [] - if self.client in ins: + if self.request in ins: # Receive client data, decode it, and queue for target bufs, closed = self.recv_frames() tqueue.extend(bufs) From 4e3388964af9697496d2c5e000bb8559dfba87ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Thu, 14 Mar 2013 15:50:49 +0100 Subject: [PATCH 02/16] Prepare for fixing https://github.com/kanaka/websockify/issues/71: Move around functions and methods, so that connection-related and server-related stuff are close together. This patch just moves things around - it does not change anything at all. This can be verified with: git diff websocket.py | grep ^- | cut -c 2- | sort > removed git diff websocket.py | grep ^+ | cut -c 2- | sort > added diff -u removed added --- websockify/websocket.py | 348 ++++++++++++++++++++-------------------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 4f298bc..5a18301 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -72,165 +72,18 @@ class WebSocketServer(object): buffer_size = 65536 + GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + server_handshake_hybi = """HTTP/1.1 101 Switching Protocols\r Upgrade: websocket\r Connection: Upgrade\r Sec-WebSocket-Accept: %s\r """ - GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - - policy_response = """\n""" - - # An exception before the WebSocket connection was established - class EClose(Exception): - pass - # An exception while the WebSocket client was connected class CClose(Exception): pass - 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, 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 - - # Make paths settings absolute - self.cert = os.path.abspath(cert) - self.key = self.web = self.record = '' - if key: - self.key = os.path.abspath(key) - if web: - self.web = os.path.abspath(web) - if record: - self.record = os.path.abspath(record) - - if self.web: - os.chdir(self.web) - - # Sanity checks - if not ssl and self.ssl_only: - raise Exception("No 'ssl' module and SSL-only specified") - if self.daemon and not resource: - raise Exception("Module 'resource' required to daemonize") - - # Show configuration - print("WebSocket server settings:") - print(" - Listen on %s:%s" % ( - self.listen_host, self.listen_port)) - print(" - Flash security policy server") - if self.web: - print(" - Web server. Web root: %s" % self.web) - if ssl: - if os.path.exists(self.cert): - print(" - SSL/TLS support") - if self.ssl_only: - print(" - Deny non-SSL/TLS connections") - else: - print(" - No SSL/TLS support (no cert file)") - else: - print(" - No SSL/TLS support (no 'ssl' module)") - if self.daemon: - print(" - Backgrounding (daemon)") - if self.record: - print(" - Recording to '%s.*'" % self.record) - - # - # WebSocketServer static methods - # - - @staticmethod - 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. - """ - flags = 0 - if host == '': - host = None - 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 - - 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 - def daemonize(keepfd=None, chdir='/'): - os.umask(0) - if chdir: - os.chdir(chdir) - else: - os.chdir('/') - os.setgid(os.getgid()) # relinquish elevations - os.setuid(os.getuid()) # relinquish elevations - - # Double fork to daemonize - if os.fork() > 0: os._exit(0) # Parent exits - os.setsid() # Obtain new process group - if os.fork() > 0: os._exit(0) # Parent exits - - # Signal handling - def terminate(a,b): os._exit(0) - signal.signal(signal.SIGTERM, terminate) - signal.signal(signal.SIGINT, signal.SIG_IGN) - - # Close open files - maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] - if maxfd == resource.RLIM_INFINITY: maxfd = 256 - for fd in reversed(range(maxfd)): - try: - if fd != keepfd: - os.close(fd) - except OSError: - _, exc, _ = sys.exc_info() - if exc.errno != errno.EBADF: raise - - # Redirect I/O to /dev/null - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) - os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) - @staticmethod def unmask(buf, hlen, plen): pstart = hlen + 4 @@ -373,30 +226,10 @@ Sec-WebSocket-Accept: %s\r return f - - # - # WebSocketServer logging/output functions - # - - def traffic(self, token="."): - """ Show traffic flow in verbose mode. """ - if self.verbose and not self.daemon: - sys.stdout.write(token) - sys.stdout.flush() - - def msg(self, msg): - """ Output message with handler_id prefix. """ - if not self.daemon: - print("% 3d: %s" % (self.handler_id, msg)) - - def vmsg(self, msg): - """ Same as msg() but only if verbose. """ - if self.verbose: - self.msg(msg) - # # Main WebSocketServer methods # + def send_frames(self, bufs=None): """ Encode and send WebSocket frames. Any frames already queued will be sent first. If buf is not set then only queued @@ -547,6 +380,156 @@ Sec-WebSocket-Accept: %s\r return response + def new_client(self): + """ Do something with a WebSockets client connection. """ + raise("WebSocketServer.new_client() must be overloaded") + + policy_response = """\n""" + + # An exception before the WebSocket connection was established + class EClose(Exception): + pass + + 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, 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 + + # Make paths settings absolute + self.cert = os.path.abspath(cert) + self.key = self.web = self.record = '' + if key: + self.key = os.path.abspath(key) + if web: + self.web = os.path.abspath(web) + if record: + self.record = os.path.abspath(record) + + if self.web: + os.chdir(self.web) + + # Sanity checks + if not ssl and self.ssl_only: + raise Exception("No 'ssl' module and SSL-only specified") + if self.daemon and not resource: + raise Exception("Module 'resource' required to daemonize") + + # Show configuration + print("WebSocket server settings:") + print(" - Listen on %s:%s" % ( + self.listen_host, self.listen_port)) + print(" - Flash security policy server") + if self.web: + print(" - Web server. Web root: %s" % self.web) + if ssl: + if os.path.exists(self.cert): + print(" - SSL/TLS support") + if self.ssl_only: + print(" - Deny non-SSL/TLS connections") + else: + print(" - No SSL/TLS support (no cert file)") + else: + print(" - No SSL/TLS support (no 'ssl' module)") + if self.daemon: + print(" - Backgrounding (daemon)") + if self.record: + print(" - Recording to '%s.*'" % self.record) + + # + # WebSocketServer static methods + # + + @staticmethod + 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. + """ + flags = 0 + if host == '': + host = None + 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 + + 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 + def daemonize(keepfd=None, chdir='/'): + os.umask(0) + if chdir: + os.chdir(chdir) + else: + os.chdir('/') + os.setgid(os.getgid()) # relinquish elevations + os.setuid(os.getuid()) # relinquish elevations + + # Double fork to daemonize + if os.fork() > 0: os._exit(0) # Parent exits + os.setsid() # Obtain new process group + if os.fork() > 0: os._exit(0) # Parent exits + + # Signal handling + def terminate(a,b): os._exit(0) + signal.signal(signal.SIGTERM, terminate) + signal.signal(signal.SIGINT, signal.SIG_IGN) + + # Close open files + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if maxfd == resource.RLIM_INFINITY: maxfd = 256 + for fd in reversed(range(maxfd)): + try: + if fd != keepfd: + os.close(fd) + except OSError: + _, exc, _ = sys.exc_info() + if exc.errno != errno.EBADF: raise + + # Redirect I/O to /dev/null + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdin.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stdout.fileno()) + os.dup2(os.open(os.devnull, os.O_RDWR), sys.stderr.fileno()) def do_handshake(self, sock, address): """ @@ -650,6 +633,27 @@ Sec-WebSocket-Accept: %s\r return retsock + # + # WebSocketServer logging/output functions + # + + def traffic(self, token="."): + """ Show traffic flow in verbose mode. """ + if self.verbose and not self.daemon: + sys.stdout.write(token) + sys.stdout.flush() + + def msg(self, msg): + """ Output message with handler_id prefix. """ + if not self.daemon: + print("% 3d: %s" % (self.handler_id, msg)) + + def vmsg(self, msg): + """ Same as msg() but only if verbose. """ + if self.verbose: + self.msg(msg) + + # # Events that can/should be overridden in sub-classes # @@ -730,10 +734,6 @@ Sec-WebSocket-Accept: %s\r # Original socket closed by caller self.request.close() - def new_client(self): - """ Do something with a WebSockets client connection. """ - raise("WebSocketServer.new_client() must be overloaded") - def start_server(self): """ Daemonize if requested. Listen for for connections. Run From 208f83b9a2b29c44c88ad31cb64ec0893ef71971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Thu, 14 Mar 2013 16:00:11 +0100 Subject: [PATCH 03/16] Prepare for fixing https://github.com/kanaka/websockify/issues/71: * Move traffic_legend. * Since websocket.WebSocketServer.socket is static, don't call it with self.socket. --- websockify/websocketproxy.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 52b46b4..928dde9 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -30,18 +30,6 @@ class WebSocketProxy(websocket.WebSocketServer): buffer_size = 65536 - traffic_legend = """ -Traffic Legend: - } - Client receive - }. - Client receive partial - { - Target receive - - > - Target send - >. - Target send partial - < - Client send - <. - Client send partial -""" - def __init__(self, *args, **kwargs): # Save off proxy specific options self.target_host = kwargs.pop('target_host', None) @@ -157,6 +145,18 @@ Traffic Legend: # will be run in a separate forked process for each connection. # + traffic_legend = """ +Traffic Legend: + } - Client receive + }. - Client receive partial + { - Target receive + + > - Target send + >. - Target send partial + < - Client send + <. - Client send partial +""" + def new_client(self): """ Called after a new WebSocket connection has been established. @@ -179,7 +179,8 @@ Traffic Legend: msg += " (using SSL)" self.msg(msg) - tsock = self.socket(self.target_host, self.target_port, + tsock = websocket.WebSocketServer.socket(self.target_host, + self.target_port, connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target) if self.verbose and not self.daemon: From 7b3dd8a6f5ef26dbfd6c34a91600ea1613aefaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Thu, 14 Mar 2013 16:07:40 +0100 Subject: [PATCH 04/16] Try to solve https://github.com/kanaka/websockify/issues/71 by refactoring. Basically, we are dividing WebSocketServer into two classes: One request handler following the SocketServer Requesthandler API, and one optional server engine. The standard Python SocketServer engine can also be used. websocketproxy.py has been updated to match the API change. I've also added a new option --libserver in order to use the Python built in server instead. I've done a lot of testing with the new code. This includes: verbose, daemon, run-once, timeout, idle-timeout, ssl, web, libserver. I've tested both Python 2 and 3. I've also tested websocket.py in another external service. Code details follows: * The new request handler class is called WebSocketRequestHandler, inheriting SimpleHTTPRequestHandler. * The service engine is called WebSocketServer, just like before. * do_websocket_handshake: Using send_header() etc, instead of manually sending HTTP response. * A new method called handle_websocket() upgrades the connection to WebSocket, if requested. Otherwise, it returns False. A typical application use is: def do_GET(self): if not self.handle_websocket(): # handle normal requests * new_client has been renamed to new_websocket_client, in order to have a better name in the SocketServer/HTTPServer request handler hierarchy. * Note that in the request handler, configuration variables must be provided by the "server" object, ie self.server.target_host. --- websockify/websocket.py | 291 +++++++++++++++++++---------------- websockify/websocketproxy.py | 112 +++++++++++--- 2 files changed, 250 insertions(+), 153 deletions(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 5a18301..68dc53e 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -64,26 +64,32 @@ if multiprocessing and sys.platform == 'win32': import multiprocessing.reduction -class WebSocketServer(object): - """ - WebSockets server class. - Must be sub-classed with new_client method definition. - """ - +# HTTP handler with WebSocket upgrade support +class WebSocketRequestHandler(SimpleHTTPRequestHandler): buffer_size = 65536 GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" - server_handshake_hybi = """HTTP/1.1 101 Switching Protocols\r -Upgrade: websocket\r -Connection: Upgrade\r -Sec-WebSocket-Accept: %s\r -""" + server_version = "WebSockify" + + protocol_version = "HTTP/1.1" # An exception while the WebSocket client was connected class CClose(Exception): pass + def __init__(self, req, addr, server): + # Retrieve a few configuration variables from the server + self.only_upgrade = getattr(server, "only_upgrade", False) + self.verbose = getattr(server, "verbose", False) + self.daemon = getattr(server, "daemon", False) + self.record = getattr(server, "record", False) + self.run_once = getattr(server, "run_once", False) + self.rec = None + self.handler_id = getattr(server, "handler_id", False) + + SimpleHTTPRequestHandler.__init__(self, req, addr, server) + @staticmethod def unmask(buf, hlen, plen): pstart = hlen + 4 @@ -204,7 +210,7 @@ Sec-WebSocket-Accept: %s\r # Process 1 frame if f['masked']: # unmask payload - f['payload'] = WebSocketServer.unmask(buf, f['hlen'], + f['payload'] = WebSocketRequestHandler.unmask(buf, f['hlen'], f['length']) else: print("Unmasked frame: %s" % repr(buf)) @@ -227,9 +233,18 @@ Sec-WebSocket-Accept: %s\r return f # - # Main WebSocketServer methods + # WebSocketRequestHandler logging/output functions # + def traffic(self, token="."): + """ Show traffic flow in verbose mode. """ + if self.verbose and not self.daemon: + sys.stdout.write(token) + sys.stdout.flush() + + # + # Main WebSocketRequestHandler methods + # def send_frames(self, bufs=None): """ Encode and send WebSocket frames. Any frames already queued will be sent first. If buf is not set then only queued @@ -311,7 +326,7 @@ Sec-WebSocket-Accept: %s\r start = frame['hlen'] end = frame['hlen'] + frame['length'] if frame['masked']: - recbuf = WebSocketServer.unmask(buf, frame['hlen'], + recbuf = WebSocketRequestHandler.unmask(buf, frame['hlen'], frame['length']) else: recbuf = buf[frame['hlen']:frame['hlen'] + @@ -336,9 +351,8 @@ Sec-WebSocket-Accept: %s\r buf, h, t = self.encode_hybi(msg, opcode=0x08, base64=False) self.request.send(buf) - def do_websocket_handshake(self, headers, path): - h = self.headers = headers - self.path = path + def do_websocket_handshake(self): + h = self.headers prot = 'WebSocket-Protocol' protocols = h.get('Sec-'+prot, h.get(prot, '')).split(',') @@ -353,7 +367,8 @@ Sec-WebSocket-Accept: %s\r if ver in ['7', '8', '13']: self.version = "hybi-%02d" % int(ver) else: - raise self.EClose('Unsupported protocol version %s' % ver) + self.send_error(400, "Unsupported protocol version %s" % ver) + return False key = h['Sec-WebSocket-Key'] @@ -363,26 +378,130 @@ Sec-WebSocket-Accept: %s\r elif 'base64' in protocols: self.base64 = True else: - raise self.EClose("Client must support 'binary' or 'base64' protocol") + self.send_error(400, "Client must support 'binary' or 'base64' protocol") + return False # Generate the hash value for the accept header accept = b64encode(sha1(s2b(key + self.GUID)).digest()) - response = self.server_handshake_hybi % b2s(accept) + self.send_response(101, "Switching Protocols") + self.send_header("Upgrade", "websocket") + self.send_header("Connection", "Upgrade") + self.send_header("Sec-WebSocket-Accept", b2s(accept)) if self.base64: - response += "Sec-WebSocket-Protocol: base64\r\n" + self.send_header("Sec-WebSocket-Protocol", "base64") else: - response += "Sec-WebSocket-Protocol: binary\r\n" - response += "\r\n" + self.send_header("Sec-WebSocket-Protocol", "binary") + self.end_headers() + return True + else: + self.send_error(400, "Missing Sec-WebSocket-Version header. Hixie protocols not supported.") + return False + + def handle_websocket(self): + """Upgrade a connection to Websocket, if requested. If this succeeds, + new_websocket_client() will be called. Otherwise, False is returned. + """ + if (self.headers.get('upgrade') and + self.headers.get('upgrade').lower() == 'websocket'): + + if not self.do_websocket_handshake(): + return False + + # Indicate to server that a Websocket upgrade was done + self.server.ws_connection = True + # Initialize per client settings + self.send_parts = [] + self.recv_part = None + self.start_time = int(time.time()*1000) + + # client_address is empty with, say, UNIX domain sockets + client_addr = "" + is_ssl = False + try: + client_addr = self.client_address[0] + is_ssl = self.client_address[2] + except IndexError: + pass + + if is_ssl: + self.stype = "SSL/TLS (wss://)" + else: + self.stype = "Plain non-SSL (ws://)" + + self.log_message("%s: %s WebSocket connection" % (client_addr, + self.stype)) + self.log_message("%s: Version %s, base64: '%s'" % (client_addr, + self.version, self.base64)) + if self.path != '/': + self.log_message("%s: Path: '%s'" % (client_addr, self.path)) + + if self.record: + # Record raw frame data as JavaScript array + fname = "%s.%s" % (self.record, + self.handler_id) + self.log_message("opening record file: %s" % fname) + self.rec = open(fname, 'w+') + encoding = "binary" + if self.base64: encoding = "base64" + self.rec.write("var VNC_frame_encoding = '%s';\n" + % encoding) + self.rec.write("var VNC_frame_data = [\n") + + try: + self.new_websocket_client() + except self.CClose: + # Close the client + _, exc, _ = sys.exc_info() + self.send_close(exc.args[0], exc.args[1]) + return True else: - raise self.EClose("Missing Sec-WebSocket-Version header. Hixie protocols not supported.") + return False - return response - - def new_client(self): + def do_GET(self): + """Handle GET request. Calls handle_websocket(). If unsuccessful, + and web server is enabled, SimpleHTTPRequestHandler.do_GET will be called.""" + if not self.handle_websocket(): + if self.only_upgrade: + self.send_error(405, "Method Not Allowed") + else: + SimpleHTTPRequestHandler.do_GET(self) + + def new_websocket_client(self): """ Do something with a WebSockets client connection. """ - raise("WebSocketServer.new_client() must be overloaded") + raise("WebSocketRequestHandler.new_websocket_client() must be overloaded") + + def do_HEAD(self): + if self.only_upgrade: + self.send_error(405, "Method Not Allowed") + else: + SimpleHTTPRequestHandler.do_HEAD(self) + + def finish(self): + if self.rec: + self.rec.write("'EOF'];\n") + self.rec.close() + + def handle(self): + # When using run_once, we have a single process, so + # we cannot loop in BaseHTTPRequestHandler.handle; we + # must return and handle new connections + if self.run_once: + self.handle_one_request() + else: + SimpleHTTPRequestHandler.handle(self) + + def log_request(self, code='-', size='-'): + if self.verbose: + SimpleHTTPRequestHandler.log_request(self, code, size) + + +class WebSocketServer(object): + """ + WebSockets server class. + Must be sub-classed with new_client method definition. + """ policy_response = """\n""" @@ -390,12 +509,14 @@ Sec-WebSocket-Accept: %s\r class EClose(Exception): pass - def __init__(self, listen_host='', listen_port=None, source_is_ipv6=False, + def __init__(self, RequestHandlerClass, 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, idle_timeout=0): # settings + self.RequestHandlerClass = RequestHandlerClass self.verbose = verbose self.listen_host = listen_host self.listen_port = listen_port @@ -422,6 +543,7 @@ Sec-WebSocket-Accept: %s\r if self.web: os.chdir(self.web) + self.only_upgrade = not self.web # Sanity checks if not ssl and self.ssl_only: @@ -548,7 +670,6 @@ 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] @@ -592,42 +713,19 @@ Sec-WebSocket-Accept: %s\r else: raise - self.scheme = "wss" - stype = "SSL/TLS (wss://)" - elif self.ssl_only: raise self.EClose("non-SSL connection received but disallowed") else: retsock = sock - self.scheme = "ws" - stype = "Plain non-SSL (ws://)" - wsh = WSRequestHandler(retsock, address, not self.web) - if wsh.last_code == 101: - # Continue on to handle WebSocket upgrade - pass - elif wsh.last_code == 405: - raise self.EClose("Normal web request received but disallowed") - elif wsh.last_code < 200 or wsh.last_code >= 300: - raise self.EClose(wsh.last_message) - elif self.verbose: - raise self.EClose(wsh.last_message) - else: - raise self.EClose("") + # If the address is like (host, port), we are extending it + # with a flag indicating SSL. Not many other options + # available... + if len(address) == 2: + address = (address[0], address[1], (retsock != sock)) - 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], - self.version, self.base64)) - if self.path != '/': - self.msg("%s: Path: '%s'" % (address[0], self.path)) - - - # Send server WebSockets handshake response - #self.msg("sending response [%s]" % response) - retsock.send(s2b(response)) + self.RequestHandlerClass(retsock, address, self) # Return the WebSockets socket which may be SSL wrapped return retsock @@ -636,13 +734,6 @@ Sec-WebSocket-Accept: %s\r # # WebSocketServer logging/output functions # - - def traffic(self, token="."): - """ Show traffic flow in verbose mode. """ - if self.verbose and not self.daemon: - sys.stdout.write(token) - sys.stdout.flush() - def msg(self, msg): """ Output message with handler_id prefix. """ if not self.daemon: @@ -683,37 +774,11 @@ Sec-WebSocket-Accept: %s\r def top_new_client(self, startsock, address): """ Do something with a WebSockets client connection. """ - # Initialize per client settings - self.send_parts = [] - self.recv_part = None - self.base64 = False - self.rec = None - self.start_time = int(time.time()*1000) - # handler process + client = None try: try: - self.request = self.do_handshake(startsock, address) - - if self.record: - # Record raw frame data as JavaScript array - fname = "%s.%s" % (self.record, - self.handler_id) - self.msg("opening record file: %s" % fname) - self.rec = open(fname, 'w+') - encoding = "binary" - if self.base64: encoding = "base64" - self.rec.write("var VNC_frame_encoding = '%s';\n" - % encoding) - self.rec.write("var VNC_frame_data = [\n") - - self.ws_connection = True - self.new_client() - except self.CClose: - # Close the client - _, exc, _ = sys.exc_info() - if self.request: - self.send_close(exc.args[0], exc.args[1]) + client = self.do_handshake(startsock, address) except self.EClose: _, exc, _ = sys.exc_info() # Connection was not a WebSockets connection @@ -725,14 +790,11 @@ Sec-WebSocket-Accept: %s\r if self.verbose: self.msg(traceback.format_exc()) finally: - if self.rec: - self.rec.write("'EOF'];\n") - self.rec.close() - if self.request and self.request != startsock: + if client and client != startsock: # Close the SSL wrapped socket # Original socket closed by caller - self.request.close() + client.close() def start_server(self): """ @@ -758,7 +820,6 @@ Sec-WebSocket-Accept: %s\r while True: try: try: - self.request = None startsock = None pid = err = 0 child_count = 0 @@ -855,33 +916,3 @@ Sec-WebSocket-Accept: %s\r self.vmsg("Closing socket listening at %s:%s" % (self.listen_host, self.listen_port)) lsock.close() - - -# HTTP handler with WebSocket upgrade support -class WSRequestHandler(SimpleHTTPRequestHandler): - def __init__(self, req, addr, only_upgrade=False): - self.only_upgrade = only_upgrade # only allow upgrades - SimpleHTTPRequestHandler.__init__(self, req, addr, object()) - - def do_GET(self): - if (self.headers.get('upgrade') and - self.headers.get('upgrade').lower() == 'websocket'): - - # Just indicate that an WebSocket upgrade is needed - self.last_code = 101 - self.last_message = "101 Switching Protocols" - elif self.only_upgrade: - # Normal web request responses are disabled - self.last_code = 405 - self.last_message = "405 Method Not Allowed" - else: - SimpleHTTPRequestHandler.do_GET(self) - - def send_response(self, code, message=None): - # Save the status code - self.last_code = code - SimpleHTTPRequestHandler.send_response(self, code, message) - - def log_message(self, f, *args): - # Save instead of printing - self.last_message = f % args diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 928dde9..f49a497 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -12,6 +12,8 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ''' import signal, socket, optparse, time, os, sys, subprocess +import SocketServer, BaseHTTPServer +from SimpleHTTPServer import SimpleHTTPRequestHandler from select import select import websocket try: @@ -20,7 +22,7 @@ except: from cgi import parse_qs from urlparse import urlparse -class WebSocketProxy(websocket.WebSocketServer): +class CustomProxyServer(websocket.WebSocketServer): """ Proxy traffic to and from a WebSockets client to a normal TCP socket server target. All traffic to/from the client is base64 @@ -140,6 +142,8 @@ class WebSocketProxy(websocket.WebSocketServer): # process. # +class ProxyRequestHandler(websocket.WebSocketRequestHandler): + # # Routines below this point are connection handler routines and # will be run in a separate forked process for each connection. @@ -150,38 +154,38 @@ Traffic Legend: } - Client receive }. - Client receive partial { - Target receive - + > - Target send >. - Target send partial < - Client send <. - Client send partial """ - def new_client(self): + def new_websocket_client(self): """ Called after a new WebSocket connection has been established. """ # Checks if we receive a token, and look # for a valid target for it then - if self.target_cfg: - (self.target_host, self.target_port) = self.get_target(self.target_cfg, self.path) + if self.server.target_cfg: + (self.server.target_host, self.server.target_port) = self.get_target(self.server.target_cfg, self.path) # Connect to the target - if self.wrap_cmd: - msg = "connecting to command: '%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) - elif self.unix_target: - msg = "connecting to unix socket: %s" % self.unix_target + if self.server.wrap_cmd: + msg = "connecting to command: '%s' (port %s)" % (" ".join(self.server.wrap_cmd), self.server.target_port) + elif self.server.unix_target: + msg = "connecting to unix socket: %s" % self.server.unix_target else: msg = "connecting to: %s:%s" % ( - self.target_host, self.target_port) + self.server.target_host, self.server.target_port) - if self.ssl_target: + if self.server.ssl_target: msg += " (using SSL)" - self.msg(msg) + self.log_message(msg) - tsock = websocket.WebSocketServer.socket(self.target_host, - self.target_port, - connect=True, use_ssl=self.ssl_target, unix_socket=self.unix_target) + tsock = websocket.WebSocketServer.socket(self.server.target_host, + self.server.target_port, + connect=True, use_ssl=self.server.ssl_target, unix_socket=self.server.unix_target) if self.verbose and not self.daemon: print(self.traffic_legend) @@ -193,8 +197,9 @@ Traffic Legend: if tsock: tsock.shutdown(socket.SHUT_RDWR) tsock.close() - self.vmsg("%s:%s: Closed target" %( - self.target_host, self.target_port)) + if self.verbose: + self.log_message("%s:%s: Closed target" %( + self.server.target_host, self.server.target_port)) raise def get_target(self, target_cfg, path): @@ -266,8 +271,9 @@ Traffic Legend: if closed: # TODO: What about blocking on client socket? - self.vmsg("%s:%s: Client closed connection" %( - self.target_host, self.target_port)) + if self.verbose: + self.log_message("%s:%s: Client closed connection" %( + self.server.target_host, self.server.target_port)) raise self.CClose(closed['code'], closed['reason']) @@ -287,8 +293,9 @@ Traffic Legend: # Receive target data, encode it and queue for client buf = target.recv(self.buffer_size) if len(buf) == 0: - self.vmsg("%s:%s: Target closed connection" %( - self.target_host, self.target_port)) + if self.verbose: + self.log_message("%s:%s: Target closed connection" %( + self.server.target_host, self.server.target_port)) raise self.CClose(1000, "Target closed") cqueue.append(buf) @@ -346,6 +353,8 @@ def websockify_init(): help="Configuration file containing valid targets " "in the form 'token: host:port' or, alternatively, a " "directory containing configuration files of this form") + parser.add_option("--libserver", action="store_true", + help="use Python library SocketServer engine") (opts, args) = parser.parse_args() # Sanity checks @@ -387,8 +396,65 @@ def websockify_init(): except: parser.error("Error parsing target port") # Create and start the WebSockets proxy - server = WebSocketProxy(**opts.__dict__) - server.start_server() + libserver = opts.libserver + del opts.libserver + if libserver: + # Use standard Python SocketServer framework + httpd = LibProxyServer(ProxyRequestHandler, **opts.__dict__) + httpd.serve_forever() + else: + # Use internal service framework + server = CustomProxyServer(ProxyRequestHandler, **opts.__dict__) + server.start_server() + + +class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): + """ + Just like CustomProxyServer, but uses standard Python SocketServer + framework. + """ + + def __init__(self, RequestHandlerClass, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + self.wrap_mode = kwargs.pop('wrap_mode', None) + self.unix_target = kwargs.pop('unix_target', None) + self.ssl_target = kwargs.pop('ssl_target', None) + self.target_cfg = kwargs.pop('target_cfg', None) + self.daemon = False + self.target_cfg = None + + # Server configuration + listen_host = kwargs.pop('listen_host', '') + listen_port = kwargs.pop('listen_port', None) + web = kwargs.pop('web', '') + + # Configuration affecting base request handler + self.only_upgrade = not web + self.verbose = kwargs.pop('verbose', False) + record = kwargs.pop('record', '') + if record: + self.record = os.path.abspath(record) + self.run_once = kwargs.pop('run_once', False) + self.handler_id = 0 + + for arg in kwargs.keys(): + print("warning: option %s ignored when using --libserver" % arg) + + if web: + os.chdir(web) + + BaseHTTPServer.HTTPServer.__init__(self, (listen_host, listen_port), + RequestHandlerClass) + + + def process_request(self, request, client_address): + """Override process_request to implement a counter""" + self.handler_id += 1 + SocketServer.ForkingMixIn.process_request(self, request, client_address) + if __name__ == '__main__': websockify_init() From 70eb75a3e69f927dbafb39f6b7cfd518e1cf4e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Mon, 18 Mar 2013 12:04:50 +0100 Subject: [PATCH 05/16] Fix error with modern Python 2.X versions: TypeError: exceptions must be old-style classes or derived from BaseException, not str Thus, we are not allowed to raise a string. Raise Exception instead. --- websockify/websocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 68dc53e..f485e8e 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -470,7 +470,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): def new_websocket_client(self): """ Do something with a WebSockets client connection. """ - raise("WebSocketRequestHandler.new_websocket_client() must be overloaded") + raise Exception("WebSocketRequestHandler.new_websocket_client() must be overloaded") def do_HEAD(self): if self.only_upgrade: From debc926612427a64bc58e694f9ce881cbfd59a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Mon, 18 Mar 2013 13:22:48 +0100 Subject: [PATCH 06/16] Renamed CustomProxyServer to WebSocketProxy; this was the earlier name. Also, call the server instance "server", not "httpd", even when using LibProxyServer. --- websockify/websocketproxy.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index f49a497..995b937 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -22,7 +22,7 @@ except: from cgi import parse_qs from urlparse import urlparse -class CustomProxyServer(websocket.WebSocketServer): +class WebSocketProxy(websocket.WebSocketServer): """ Proxy traffic to and from a WebSockets client to a normal TCP socket server target. All traffic to/from the client is base64 @@ -400,17 +400,16 @@ def websockify_init(): del opts.libserver if libserver: # Use standard Python SocketServer framework - httpd = LibProxyServer(ProxyRequestHandler, **opts.__dict__) - httpd.serve_forever() + server = LibProxyServer(ProxyRequestHandler, **opts.__dict__) else: # Use internal service framework - server = CustomProxyServer(ProxyRequestHandler, **opts.__dict__) - server.start_server() + server = WebSocketProxy(ProxyRequestHandler, **opts.__dict__) + server.start_server() class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): """ - Just like CustomProxyServer, but uses standard Python SocketServer + Just like WebSocketProxy, but uses standard Python SocketServer framework. """ From b05b773bd7bb4dba2b834dc70fc2f62ad3a248f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Mon, 18 Mar 2013 13:25:53 +0100 Subject: [PATCH 07/16] Corrected last commit. --- websockify/websocketproxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 995b937..46f8efc 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -401,10 +401,11 @@ def websockify_init(): if libserver: # Use standard Python SocketServer framework server = LibProxyServer(ProxyRequestHandler, **opts.__dict__) + server.serve_forever() else: # Use internal service framework server = WebSocketProxy(ProxyRequestHandler, **opts.__dict__) - server.start_server() + server.start_server() class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): From 09f3ec7125e49fb396a4747042eee2acffb2c3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 09:03:18 +0100 Subject: [PATCH 08/16] Rename self.client to self.request, ie adapt to: >commit b9e1295f7afa34a4c69618609e053af154e477d7 > Prepare for solving https://github.com/kanaka/websockify/issues/71: > > Rename self.client to self.request, since this is what standard > SocketServer request handlers are using. --- tests/echo.py | 8 ++++---- tests/load.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/echo.py b/tests/echo.py index d79553e..d151bfb 100755 --- a/tests/echo.py +++ b/tests/echo.py @@ -28,21 +28,21 @@ class WebSocketEcho(WebSocketServer): cqueue = [] c_pend = 0 cpartial = "" - rlist = [self.client] + rlist = [self.request] while True: wlist = [] - if cqueue or c_pend: wlist.append(self.client) + if cqueue or c_pend: wlist.append(self.request) ins, outs, excepts = select.select(rlist, wlist, [], 1) if excepts: raise Exception("Socket exception") - if self.client in outs: + if self.request in outs: # Send queued target data to the client c_pend = self.send_frames(cqueue) cqueue = [] - if self.client in ins: + if self.request in ins: # Receive client data, decode it, and send it back frames, closed = self.recv_frames() cqueue.extend(frames) diff --git a/tests/load.py b/tests/load.py index 0da7265..ba2ebde 100755 --- a/tests/load.py +++ b/tests/load.py @@ -34,7 +34,7 @@ class WebSocketLoad(WebSocketServer): self.recv_cnt = 0 try: - self.responder(self.client) + self.responder(self.request) except: print "accumulated errors:", self.errors self.errors = 0 From d0608a63b6290949f5bf0fd115db11eb21b4883c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 10:03:04 +0100 Subject: [PATCH 09/16] Make echo.py and load.py work again, after the refactoring of websocket.py: * With echo.py, which doesn't need any server configuration, we can just switch over our application class to inherit from WebSocketRequestHandler instead of WebSocketServer. Also, need to use the new method name new_websocket_client. * With load.py, since we have the "delay" configuration, we need both a server class and a request handler. Note that for both tests, I've removed the raising of self.EClose(closed). This is incorrect. First of all, it's described as "An exception before the WebSocket connection was established", so not suitable for our case. Second, it will cause send_close to be called twice. Finally, self.EClose is now in the WebSocketServer class, and not a member of the request handler. --- tests/echo.py | 9 ++++----- tests/load.py | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/echo.py b/tests/echo.py index d151bfb..9b1d9a6 100755 --- a/tests/echo.py +++ b/tests/echo.py @@ -12,15 +12,15 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates import os, sys, select, optparse sys.path.insert(0,os.path.dirname(__file__) + "/../websockify") -from websocket import WebSocketServer +from websocket import WebSocketServer, WebSocketRequestHandler -class WebSocketEcho(WebSocketServer): +class WebSocketEcho(WebSocketRequestHandler): """ WebSockets server that echos back whatever is received from the client. """ buffer_size = 8096 - def new_client(self): + def new_websocket_client(self): """ Echo back whatever is received. """ @@ -49,7 +49,6 @@ class WebSocketEcho(WebSocketServer): if closed: self.send_close() - raise self.EClose(closed) if __name__ == '__main__': parser = optparse.OptionParser(usage="%prog [options] listen_port") @@ -70,6 +69,6 @@ if __name__ == '__main__': parser.error("Invalid arguments") opts.web = "." - server = WebSocketEcho(**opts.__dict__) + server = WebSocketServer(WebSocketEcho, **opts.__dict__) server.start_server() diff --git a/tests/load.py b/tests/load.py index ba2ebde..bd32e68 100755 --- a/tests/load.py +++ b/tests/load.py @@ -8,28 +8,30 @@ given a sequence number. Any errors are reported and counted. import sys, os, select, random, time, optparse sys.path.insert(0,os.path.dirname(__file__) + "/../websockify") -from websocket import WebSocketServer +from websocket import WebSocketServer, WebSocketRequestHandler -class WebSocketLoad(WebSocketServer): +class WebSocketLoadServer(WebSocketServer): - buffer_size = 65536 - - max_packet_size = 10000 recv_cnt = 0 send_cnt = 0 def __init__(self, *args, **kwargs): - self.errors = 0 self.delay = kwargs.pop('delay') + WebSocketServer.__init__(self, *args, **kwargs) + + +class WebSocketLoad(WebSocketRequestHandler): + + max_packet_size = 10000 + + def new_websocket_client(self): print "Prepopulating random array" self.rand_array = [] for i in range(0, self.max_packet_size): self.rand_array.append(random.randint(0, 9)) - WebSocketServer.__init__(self, *args, **kwargs) - - def new_client(self): + self.errors = 0 self.send_cnt = 0 self.recv_cnt = 0 @@ -61,14 +63,13 @@ class WebSocketLoad(WebSocketServer): if closed: self.send_close() - raise self.EClose(closed) now = time.time() * 1000 if client in outs: if c_pend: last_send = now c_pend = self.send_frames() - elif now > (last_send + self.delay): + elif now > (last_send + self.server.delay): last_send = now c_pend = self.send_frames([self.generate()]) @@ -162,6 +163,6 @@ if __name__ == '__main__': parser.error("Invalid arguments") opts.web = "." - server = WebSocketLoad(**opts.__dict__) + server = WebSocketLoadServer(WebSocketLoad, **opts.__dict__) server.start_server() From f594d70daf960fd2fb08165983c206be18b3de9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 11:00:34 +0100 Subject: [PATCH 10/16] Removed unused import of SimpleHTTPRequestHandler. --- websockify/websocketproxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 46f8efc..0bd56c7 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -13,7 +13,6 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates import signal, socket, optparse, time, os, sys, subprocess import SocketServer, BaseHTTPServer -from SimpleHTTPServer import SimpleHTTPRequestHandler from select import select import websocket try: From f5e42ff6f4a25630cee8ab6a314552cf7d53829a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 11:30:38 +0100 Subject: [PATCH 11/16] Move WebSocketProxy class so that it is defined after the requesthandler. Removed comments about above/below which does not make sense any longer. No functional changes. --- websockify/websocketproxy.py | 239 +++++++++++++++++------------------ 1 file changed, 114 insertions(+), 125 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index 0bd56c7..c1bf839 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -21,133 +21,8 @@ except: from cgi import parse_qs from urlparse import urlparse -class WebSocketProxy(websocket.WebSocketServer): - """ - Proxy traffic to and from a WebSockets client to a normal TCP - socket server target. All traffic to/from the client is base64 - encoded/decoded to allow binary data to be sent/received to/from - the target. - """ - - buffer_size = 65536 - - def __init__(self, *args, **kwargs): - # Save off proxy specific options - self.target_host = kwargs.pop('target_host', None) - self.target_port = kwargs.pop('target_port', None) - self.wrap_cmd = kwargs.pop('wrap_cmd', None) - self.wrap_mode = kwargs.pop('wrap_mode', None) - self.unix_target = kwargs.pop('unix_target', None) - self.ssl_target = kwargs.pop('ssl_target', None) - self.target_cfg = kwargs.pop('target_cfg', None) - # Last 3 timestamps command was run - self.wrap_times = [0, 0, 0] - - if self.wrap_cmd: - rebinder_path = ['./', os.path.dirname(sys.argv[0])] - self.rebinder = None - - for rdir in rebinder_path: - rpath = os.path.join(rdir, "rebind.so") - if os.path.exists(rpath): - self.rebinder = rpath - break - - if not self.rebinder: - raise Exception("rebind.so not found, perhaps you need to run make") - self.rebinder = os.path.abspath(self.rebinder) - - self.target_host = "127.0.0.1" # Loopback - # Find a free high port - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.bind(('', 0)) - self.target_port = sock.getsockname()[1] - sock.close() - - os.environ.update({ - "LD_PRELOAD": self.rebinder, - "REBIND_OLD_PORT": str(kwargs['listen_port']), - "REBIND_NEW_PORT": str(self.target_port)}) - - if self.target_cfg: - self.target_cfg = os.path.abspath(self.target_cfg) - - websocket.WebSocketServer.__init__(self, *args, **kwargs) - - def run_wrap_cmd(self): - print("Starting '%s'" % " ".join(self.wrap_cmd)) - self.wrap_times.append(time.time()) - self.wrap_times.pop(0) - self.cmd = subprocess.Popen( - self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) - self.spawn_message = True - - def started(self): - """ - Called after Websockets server startup (i.e. after daemonize) - """ - # Need to call wrapped command after daemonization so we can - # know when the wrapped command exits - if self.wrap_cmd: - dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) - elif self.unix_target: - dst_string = self.unix_target - else: - dst_string = "%s:%s" % (self.target_host, self.target_port) - - if self.target_cfg: - msg = " - proxying from %s:%s to targets in %s" % ( - self.listen_host, self.listen_port, self.target_cfg) - else: - msg = " - proxying from %s:%s to %s" % ( - self.listen_host, self.listen_port, dst_string) - - if self.ssl_target: - msg += " (using SSL)" - - print(msg + "\n") - - if self.wrap_cmd: - self.run_wrap_cmd() - - def poll(self): - # If we are wrapping a command, check it's status - - if self.wrap_cmd and self.cmd: - ret = self.cmd.poll() - if ret != None: - self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) - self.cmd = None - - if self.wrap_cmd and self.cmd == None: - # Response to wrapped command being gone - if self.wrap_mode == "ignore": - pass - elif self.wrap_mode == "exit": - sys.exit(ret) - elif self.wrap_mode == "respawn": - now = time.time() - avg = sum(self.wrap_times)/len(self.wrap_times) - if (now - avg) < 10: - # 3 times in the last 10 seconds - if self.spawn_message: - print("Command respawning too fast") - self.spawn_message = False - else: - self.run_wrap_cmd() - - # - # Routines above this point are run in the master listener - # process. - # - class ProxyRequestHandler(websocket.WebSocketRequestHandler): - # - # Routines below this point are connection handler routines and - # will be run in a separate forked process for each connection. - # - traffic_legend = """ Traffic Legend: } - Client receive @@ -300,6 +175,120 @@ Traffic Legend: cqueue.append(buf) self.traffic("{") +class WebSocketProxy(websocket.WebSocketServer): + """ + Proxy traffic to and from a WebSockets client to a normal TCP + socket server target. All traffic to/from the client is base64 + encoded/decoded to allow binary data to be sent/received to/from + the target. + """ + + buffer_size = 65536 + + def __init__(self, *args, **kwargs): + # Save off proxy specific options + self.target_host = kwargs.pop('target_host', None) + self.target_port = kwargs.pop('target_port', None) + self.wrap_cmd = kwargs.pop('wrap_cmd', None) + self.wrap_mode = kwargs.pop('wrap_mode', None) + self.unix_target = kwargs.pop('unix_target', None) + self.ssl_target = kwargs.pop('ssl_target', None) + self.target_cfg = kwargs.pop('target_cfg', None) + # Last 3 timestamps command was run + self.wrap_times = [0, 0, 0] + + if self.wrap_cmd: + rebinder_path = ['./', os.path.dirname(sys.argv[0])] + self.rebinder = None + + for rdir in rebinder_path: + rpath = os.path.join(rdir, "rebind.so") + if os.path.exists(rpath): + self.rebinder = rpath + break + + if not self.rebinder: + raise Exception("rebind.so not found, perhaps you need to run make") + self.rebinder = os.path.abspath(self.rebinder) + + self.target_host = "127.0.0.1" # Loopback + # Find a free high port + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('', 0)) + self.target_port = sock.getsockname()[1] + sock.close() + + os.environ.update({ + "LD_PRELOAD": self.rebinder, + "REBIND_OLD_PORT": str(kwargs['listen_port']), + "REBIND_NEW_PORT": str(self.target_port)}) + + if self.target_cfg: + self.target_cfg = os.path.abspath(self.target_cfg) + + websocket.WebSocketServer.__init__(self, *args, **kwargs) + + def run_wrap_cmd(self): + print("Starting '%s'" % " ".join(self.wrap_cmd)) + self.wrap_times.append(time.time()) + self.wrap_times.pop(0) + self.cmd = subprocess.Popen( + self.wrap_cmd, env=os.environ, preexec_fn=_subprocess_setup) + self.spawn_message = True + + def started(self): + """ + Called after Websockets server startup (i.e. after daemonize) + """ + # Need to call wrapped command after daemonization so we can + # know when the wrapped command exits + if self.wrap_cmd: + dst_string = "'%s' (port %s)" % (" ".join(self.wrap_cmd), self.target_port) + elif self.unix_target: + dst_string = self.unix_target + else: + dst_string = "%s:%s" % (self.target_host, self.target_port) + + if self.target_cfg: + msg = " - proxying from %s:%s to targets in %s" % ( + self.listen_host, self.listen_port, self.target_cfg) + else: + msg = " - proxying from %s:%s to %s" % ( + self.listen_host, self.listen_port, dst_string) + + if self.ssl_target: + msg += " (using SSL)" + + print(msg + "\n") + + if self.wrap_cmd: + self.run_wrap_cmd() + + def poll(self): + # If we are wrapping a command, check it's status + + if self.wrap_cmd and self.cmd: + ret = self.cmd.poll() + if ret != None: + self.vmsg("Wrapped command exited (or daemon). Returned %s" % ret) + self.cmd = None + + if self.wrap_cmd and self.cmd == None: + # Response to wrapped command being gone + if self.wrap_mode == "ignore": + pass + elif self.wrap_mode == "exit": + sys.exit(ret) + elif self.wrap_mode == "respawn": + now = time.time() + avg = sum(self.wrap_times)/len(self.wrap_times) + if (now - avg) < 10: + # 3 times in the last 10 seconds + if self.spawn_message: + print("Command respawning too fast") + self.spawn_message = False + else: + self.run_wrap_cmd() def _subprocess_setup(): From e964c1edff1e1dc12845f14755c5303897e63252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 11:34:46 +0100 Subject: [PATCH 12/16] Let our ProxyRequestHandler be default. This allows you to inherit from WebSocketProxy without having to specify handler class. --- websockify/websocketproxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index c1bf839..e59f084 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -185,7 +185,7 @@ class WebSocketProxy(websocket.WebSocketServer): buffer_size = 65536 - def __init__(self, *args, **kwargs): + def __init__(self, RequestHandlerClass=ProxyRequestHandler, *args, **kwargs): # Save off proxy specific options self.target_host = kwargs.pop('target_host', None) self.target_port = kwargs.pop('target_port', None) @@ -226,7 +226,7 @@ class WebSocketProxy(websocket.WebSocketServer): if self.target_cfg: self.target_cfg = os.path.abspath(self.target_cfg) - websocket.WebSocketServer.__init__(self, *args, **kwargs) + websocket.WebSocketServer.__init__(self, RequestHandlerClass, *args, **kwargs) def run_wrap_cmd(self): print("Starting '%s'" % " ".join(self.wrap_cmd)) @@ -388,11 +388,11 @@ def websockify_init(): del opts.libserver if libserver: # Use standard Python SocketServer framework - server = LibProxyServer(ProxyRequestHandler, **opts.__dict__) + server = LibProxyServer(**opts.__dict__) server.serve_forever() else: # Use internal service framework - server = WebSocketProxy(ProxyRequestHandler, **opts.__dict__) + server = WebSocketProxy(**opts.__dict__) server.start_server() @@ -402,7 +402,7 @@ class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): framework. """ - def __init__(self, RequestHandlerClass, **kwargs): + def __init__(self, RequestHandlerClass=ProxyRequestHandler, **kwargs): # Save off proxy specific options self.target_host = kwargs.pop('target_host', None) self.target_port = kwargs.pop('target_port', None) From bc917863e0282beb15ebe26da91de007e5d1bedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 13:30:16 +0100 Subject: [PATCH 13/16] Improved class documentation. --- websockify/websocket.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index f485e8e..49e53f6 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -66,6 +66,20 @@ if multiprocessing and sys.platform == 'win32': # HTTP handler with WebSocket upgrade support class WebSocketRequestHandler(SimpleHTTPRequestHandler): + """ + WebSocket Request Handler Class, derived from SimpleHTTPRequestHandler. + Must be sub-classed with new_websocket_client method definition. + The request handler can be configured by setting optional + attributes on the server object: + + * only_upgrade: If true, SimpleHTTPRequestHandler will not be enabled, + only websocket is allowed. + * verbose: If true, verbose logging is activated. + * daemon: Running as daemon, do not write to console etc + * record: Record raw frame data as JavaScript array into specified filename + * run_once: Handle a single request + * handler_id: A sequence number for this connection, appended to record filename + """ buffer_size = 65536 GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" @@ -500,7 +514,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): class WebSocketServer(object): """ WebSockets server class. - Must be sub-classed with new_client method definition. + As an alternative, the standard library SocketServer can be used """ policy_response = """\n""" From ed109d7ec8c525cb65491d913f60cca3078a4f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Wed, 20 Mar 2013 15:09:58 +0100 Subject: [PATCH 14/16] Fix Python3 compatibility when using --libserver. --- websockify/websocketproxy.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index e59f084..e2110d7 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -12,7 +12,10 @@ as taken from http://docs.python.org/dev/library/ssl.html#certificates ''' import signal, socket, optparse, time, os, sys, subprocess -import SocketServer, BaseHTTPServer +try: from socketserver import ForkingMixIn +except: from SocketServer import ForkingMixIn +try: from http.server import HTTPServer +except: from BaseHTTPServer import HTTPServer from select import select import websocket try: @@ -396,7 +399,7 @@ def websockify_init(): server.start_server() -class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): +class LibProxyServer(ForkingMixIn, HTTPServer): """ Just like WebSocketProxy, but uses standard Python SocketServer framework. @@ -434,14 +437,14 @@ class LibProxyServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): if web: os.chdir(web) - BaseHTTPServer.HTTPServer.__init__(self, (listen_host, listen_port), - RequestHandlerClass) + HTTPServer.__init__(self, (listen_host, listen_port), + RequestHandlerClass) def process_request(self, request, client_address): """Override process_request to implement a counter""" self.handler_id += 1 - SocketServer.ForkingMixIn.process_request(self, request, client_address) + ForkingMixIn.process_request(self, request, client_address) if __name__ == '__main__': From db933950617c14ef91490b3223a1f1cf848769a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Thu, 28 Nov 2013 12:37:57 +0100 Subject: [PATCH 15/16] Follow up on 131f9ea645ac6f00d98743a420d168033f99063a: Proper logging in request handler class. --- websockify/websocket.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 210ef09..ac5184b 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -104,6 +104,10 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): self.handler_id = getattr(server, "handler_id", False) self.file_only = getattr(server, "file_only", False) self.traffic = getattr(server, "traffic", False) + + self.logger = getattr(server, "logger", None) + if self.logger is None: + self.logger = WebSocketServer.get_logger() SimpleHTTPRequestHandler.__init__(self, req, addr, server) @@ -265,17 +269,17 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): def msg(self, msg, *args, **kwargs): """ Output message with handler_id prefix. """ prefix = "% 3d: " % self.handler_id - self.server.msg("%s%s" % (prefix, msg), *args, **kwargs) + self.logger.log(logging.INFO, "%s%s" % (prefix, msg), *args, **kwargs) def vmsg(self, msg, *args, **kwargs): """ Same as msg() but as debug. """ prefix = "% 3d: " % self.handler_id - self.server.vmsg("%s%s" % (prefix, msg), *args, **kwargs) + self.logger.log(logging.DEBUG, "%s%s" % (prefix, msg), *args, **kwargs) def warn(self, msg, *args, **kwargs): """ Same as msg() but as warning. """ prefix = "% 3d: " % self.handler_id - self.server.warn("%s%s" % (prefix, msg), *args, **kwargs) + self.logger.log(logging.WARN, "%s%s" % (prefix, msg), *args, **kwargs) # # Main WebSocketRequestHandler methods From 6de69338191f39745b045ed8a6b9349735235090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C3=85strand=20=28astrand=29?= Date: Tue, 17 Dec 2013 14:20:14 +0100 Subject: [PATCH 16/16] Minor whitespace and layout tweaks, to reduce diff against upstream/master. --- websockify/websocket.py | 7 ++++--- websockify/websocketproxy.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/websockify/websocket.py b/websockify/websocket.py index 700b7d7..889bc40 100644 --- a/websockify/websocket.py +++ b/websockify/websocket.py @@ -256,6 +256,7 @@ class WebSocketRequestHandler(SimpleHTTPRequestHandler): return f + # # WebSocketRequestHandler logging/output functions # @@ -562,7 +563,8 @@ class WebSocketServer(object): def __init__(self, RequestHandlerClass, listen_host='', listen_port=None, source_is_ipv6=False, verbose=False, cert='', key='', ssl_only=None, - daemon=False, record='', web='', file_only=False, + daemon=False, record='', web='', + file_only=False, run_once=False, timeout=0, idle_timeout=0, traffic=False, tcp_keepalive=True, tcp_keepcnt=None, tcp_keepidle=None, tcp_keepintvl=None): @@ -809,7 +811,6 @@ class WebSocketServer(object): # Return the WebSockets socket which may be SSL wrapped return retsock - # # WebSocketServer logging/output functions # @@ -1024,6 +1025,6 @@ class WebSocketServer(object): # Restore signals for sig, func in original_signals.items(): - signal.signal(sig, func) + signal.signal(sig, func) diff --git a/websockify/websocketproxy.py b/websockify/websocketproxy.py index e8bbf02..f51cc7c 100755 --- a/websockify/websocketproxy.py +++ b/websockify/websocketproxy.py @@ -31,7 +31,7 @@ Traffic Legend: } - Client receive }. - Client receive partial { - Target receive - + > - Target send >. - Target send partial < - Client send