From dc9d9135cbbeaa8b5ba2ce199fd4424ce14f6164 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 24 Jun 2015 16:59:23 +0100 Subject: [PATCH 001/141] Check the opcode of the frame We implement a system of opcode checking so we can differentiate the types of frame we are receiving. In case the opcode is unknown, we close the connection according to RFC 6455: "If an unknown opcode is received, the receiving endpoint MUST _Fail the WebSocket Connection_." However, if the opcode is know but we do not support it, we just display a warning and ignore the frame. For now on, nothing happens if we receive a PING or PONG frame. fixed #6 Signed-off-by: Olivier Gayot --- websocket_server/websocket_server.py | 36 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 550099b..7f8635a 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -37,9 +37,12 @@ PAYLOAD_LEN_EXT16 = 0x7e PAYLOAD_LEN_EXT64 = 0x7f -OPCODE_TEXT = 0x01 -CLOSE_CONN = 0x8 - +OPCODE_CONTINUATION = 0x0 +OPCODE_TEXT = 0x1 +OPCODE_BINARY = 0x2 +OPCODE_CLOSE_CONN = 0x8 +OPCODE_PING = 0x9 +OPCODE_PONG = 0xA # -------------------------------- API --------------------------------- @@ -99,6 +102,12 @@ def __init__(self, port, host='127.0.0.1'): def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) + def _ping_received_(self, handler, msg): + pass + + def _pong_received_(self, handler, msg): + pass + def _new_client_(self, handler): self.id_counter += 1 client={ @@ -164,12 +173,13 @@ def read_next_message(self): opcode = b1 & OPCODE masked = b2 & MASKED payload_length = b2 & PAYLOAD_LEN + opcode_handler = None if not b1: print("Client closed connection.") self.keep_alive = 0 return - if opcode == CLOSE_CONN: + if opcode == OPCODE_CLOSE_CONN: print("Client asked to close connection.") self.keep_alive = 0 return @@ -177,6 +187,22 @@ def read_next_message(self): print("Client must always be masked.") self.keep_alive = 0 return + if opcode == OPCODE_CONTINUATION: + print("Continuation frames not handled.") + return + elif opcode == OPCODE_BINARY: + print("Binary frames not handled.") + return + elif opcode == OPCODE_TEXT: + opcode_handler = self.server._message_received_ + elif opcode == OPCODE_PING: + opcode_handler = self.server._ping_received_ + elif opcode == OPCODE_PONG: + opcode_handler = self.server._pong_received_ + else: + print("Unknown opcode %#x." + opcode) + self.keep_alive = 0 + return if payload_length == 126: payload_length = struct.unpack(">H", self.rfile.read(2))[0] @@ -188,7 +214,7 @@ def read_next_message(self): for char in self.read_bytes(payload_length): char ^= masks[len(decoded) % 4] decoded += chr(char) - self.server._message_received_(self, decoded) + opcode_handler(self, decoded) def send_message(self, message): self.send_text(message) From 941c913742ff9c8290b9945c9220f7531a37996e Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 24 Jun 2015 17:13:21 +0100 Subject: [PATCH 002/141] Handle the PING frames Whenever we receive a PING frame, we have to reply with a PONG frame with a payload identical to the one present in the received PING. we reuse the function send_text() to send the pong frame but add the opcode to be sent as an argument. This opcode defaults to OPCODE_TEXT so we do not break the compatibility. fixes #5 Signed-off-by: Olivier Gayot --- websocket_server/websocket_server.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 7f8635a..db6a5cd 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -103,7 +103,7 @@ def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) def _ping_received_(self, handler, msg): - pass + handler.send_pong(msg) def _pong_received_(self, handler, msg): pass @@ -219,7 +219,10 @@ def read_next_message(self): def send_message(self, message): self.send_text(message) - def send_text(self, message): + def send_pong(self, message): + self.send_text(message, OPCODE_PONG) + + def send_text(self, message, opcode=OPCODE_TEXT): ''' NOTES Fragmented(=continuation) messages are not being used since their usage @@ -244,18 +247,18 @@ def send_text(self, message): # Normal payload if payload_length <= 125: - header.append(FIN | OPCODE_TEXT) + header.append(FIN | opcode) header.append(payload_length) # Extended payload elif payload_length >= 126 and payload_length <= 65535: - header.append(FIN | OPCODE_TEXT) + header.append(FIN | opcode) header.append(PAYLOAD_LEN_EXT16) header.extend(struct.pack(">H", payload_length)) # Huge extended payload elif payload_length < 18446744073709551616: - header.append(FIN | OPCODE_TEXT) + header.append(FIN | opcode) header.append(PAYLOAD_LEN_EXT64) header.extend(struct.pack(">Q", payload_length)) From ae6cf8827d6be37fd5da5285285a1202d0b01e82 Mon Sep 17 00:00:00 2001 From: Orange Tsai Date: Mon, 7 Mar 2016 18:16:54 +0800 Subject: [PATCH 003/141] fixed force close error When a client force close(such as CTRL+C), websocket server will show error because can't get next message. --- websocket_server/websocket_server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 550099b..bf93256 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -157,8 +157,10 @@ def read_bytes(self, num): return bytes def read_next_message(self): - - b1, b2 = self.read_bytes(2) + try: + b1, b2 = self.read_bytes(2) + except ValueError as e: + b1, b2 = 0, 0 fin = b1 & FIN opcode = b1 & OPCODE From 1fbb59b89bbbef85b284f2a0298d00d96d12cbd6 Mon Sep 17 00:00:00 2001 From: aeroaks Date: Wed, 20 Apr 2016 11:28:59 +0200 Subject: [PATCH 004/141] Added example for server IP --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb12100..e84e904 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The API is simply methods and properties of the `WebsocketServer` class. The WebsocketServer takes two arguments: a `port` and a `hostname`. By default `localhost` is used. However if you want to be able and connect -to the server from the network you need to pass `0.0.0.0` as hostname. +to the server from the network you need to pass `0.0.0.0` as hostname. e.g., WebsocketServer(13254, '127.0.0.2') for custom IP. ###Properties @@ -81,7 +81,7 @@ from websocket_server import WebsocketServer def new_client(client, server): server.send_message_to_all("Hey all, a new client has joined us") -server = WebsocketServer(13254) +server = WebsocketServer(13254, host='127.0.0.1') server.set_fn_new_client(new_client) server.run_forever() ```` From dd5c150d0bd9d7843bd50e66924f6bbe32d28628 Mon Sep 17 00:00:00 2001 From: ha-D Date: Mon, 29 Aug 2016 13:07:29 -0400 Subject: [PATCH 005/141] Use logging module instead of prints --- websocket_server/websocket_server.py | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index bf93256..8948803 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -5,6 +5,7 @@ import struct from base64 import b64encode from hashlib import sha1 +import logging if sys.version_info[0] < 3 : from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler @@ -41,19 +42,21 @@ CLOSE_CONN = 0x8 +# ------------------------------ Logging ------------------------------- +logger = logging.getLogger(__name__) # -------------------------------- API --------------------------------- class API(): def run_forever(self): try: - print("Listening on port %d for clients.." % self.port) + logger.info("Listening on port %d for clients.." % self.port) self.serve_forever() except KeyboardInterrupt: self.server_close() - print("Server terminated.") + logger.info("Server terminated.") except Exception as e: - print("ERROR: WebSocketsServer: "+str(e)) + logger.error("ERROR: WebSocketsServer: " + str(e), exc_info=True) exit(1) def new_client(self, client, server): pass @@ -114,14 +117,14 @@ def _client_left_(self, handler): self.client_left(client, self) if client in self.clients: self.clients.remove(client) - + def _unicast_(self, to_client, msg): to_client['handler'].send_message(msg) def _multicast_(self, msg): for client in self.clients: self._unicast_(client, msg) - + def handler_to_client(self, handler): for client in self.clients: if client['handler'] == handler: @@ -168,15 +171,15 @@ def read_next_message(self): payload_length = b2 & PAYLOAD_LEN if not b1: - print("Client closed connection.") + logger.info("Client closed connection.") self.keep_alive = 0 return if opcode == CLOSE_CONN: - print("Client asked to close connection.") + logger.info("Client asked to close connection.") self.keep_alive = 0 return if not masked: - print("Client must always be masked.") + logger.info("Client must always be masked.") self.keep_alive = 0 return @@ -201,17 +204,17 @@ def send_text(self, message): Fragmented(=continuation) messages are not being used since their usage is needed in very limited cases - when we don't know the payload length. ''' - + # Validate message if isinstance(message, bytes): message = try_decode_UTF8(message) # this is slower but assures we have UTF-8 if not message: - print("Can\'t send message, message is not valid UTF-8") + logger.warning("Can\'t send message, message is not valid UTF-8") return False elif isinstance(message, str) or isinstance(message, unicode): pass else: - print('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) return False header = bytearray() @@ -234,7 +237,7 @@ def send_text(self, message): header.append(FIN | OPCODE_TEXT) header.append(PAYLOAD_LEN_EXT64) header.extend(struct.pack(">Q", payload_length)) - + else: raise Exception("Message is too big. Consider breaking it into chunks.") return @@ -251,14 +254,14 @@ def handshake(self): if key: key = key.group(1) else: - print("Client tried to connect but was missing a key") + logging.warning("Client tried to connect but was missing a key") self.keep_alive = False return response = self.make_handshake_response(key) self.handshake_done = self.request.send(response.encode()) self.valid_client = True self.server._new_client_(self) - + def make_handshake_response(self, key): return \ 'HTTP/1.1 101 Switching Protocols\r\n'\ @@ -266,7 +269,7 @@ def make_handshake_response(self, key): 'Connection: Upgrade\r\n' \ 'Sec-WebSocket-Accept: %s\r\n' \ '\r\n' % self.calculate_response_key(key) - + def calculate_response_key(self, key): GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' hash = sha1(key.encode() + GUID.encode()) @@ -282,7 +285,7 @@ def encode_to_UTF8(data): try: return data.encode('UTF-8') except UnicodeEncodeError as e: - print("Could not encode data to UTF-8 -- %s" % e) + logging.error("Could not encode data to UTF-8 -- %s" % e) return False except Exception as e: raise(e) @@ -297,7 +300,7 @@ def try_decode_UTF8(data): return False except Exception as e: raise(e) - + # This is only for testing purposes From 4370d05d72444baab079c59e752ded63d4e08553 Mon Sep 17 00:00:00 2001 From: ha-D Date: Fri, 2 Sep 2016 13:54:04 -0400 Subject: [PATCH 006/141] Fix logging error --- websocket_server/websocket_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 8948803..e48fd33 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -254,7 +254,7 @@ def handshake(self): if key: key = key.group(1) else: - logging.warning("Client tried to connect but was missing a key") + logger.warning("Client tried to connect but was missing a key") self.keep_alive = False return response = self.make_handshake_response(key) @@ -285,7 +285,7 @@ def encode_to_UTF8(data): try: return data.encode('UTF-8') except UnicodeEncodeError as e: - logging.error("Could not encode data to UTF-8 -- %s" % e) + logger.error("Could not encode data to UTF-8 -- %s" % e) return False except Exception as e: raise(e) From c082baefb239e7e1d65d975a81fe4be3c4ddb74d Mon Sep 17 00:00:00 2001 From: Johan Date: Sat, 8 Oct 2016 19:29:14 +0100 Subject: [PATCH 007/141] Update README.md Small fixes to documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e84e904..1ebecfa 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The API is simply methods and properties of the `WebsocketServer` class. The WebsocketServer takes two arguments: a `port` and a `hostname`. By default `localhost` is used. However if you want to be able and connect -to the server from the network you need to pass `0.0.0.0` as hostname. e.g., WebsocketServer(13254, '127.0.0.2') for custom IP. +to the server from the network you need to pass `0.0.0.0` as hostname e.g. `WebsocketServer(13254, '0.0.0.0')`. ###Properties From 301e4221f666edbe81d0a8e894385f11fa2fb6af Mon Sep 17 00:00:00 2001 From: Johan Date: Sat, 8 Oct 2016 19:30:07 +0100 Subject: [PATCH 008/141] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ebecfa..b55e559 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The API is simply methods and properties of the `WebsocketServer` class. The WebsocketServer takes two arguments: a `port` and a `hostname`. By default `localhost` is used. However if you want to be able and connect -to the server from the network you need to pass `0.0.0.0` as hostname e.g. `WebsocketServer(13254, '0.0.0.0')`. +to the server from the network you need to pass `0.0.0.0` as hostname e.g. `WebsocketServer(13254, host='0.0.0.0')`. ###Properties From d59cd0fa896c204867bb0505a27956e98fb226aa Mon Sep 17 00:00:00 2001 From: Johan Date: Sat, 8 Oct 2016 19:33:58 +0100 Subject: [PATCH 009/141] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b55e559..9bedcae 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ The API is simply methods and properties of the `WebsocketServer` class. ## WebsocketServer The WebsocketServer takes two arguments: a `port` and a `hostname`. -By default `localhost` is used. However if you want to be able and connect +By default the localhost `127.0.0.1` is used. However if you want to be able and connect to the server from the network you need to pass `0.0.0.0` as hostname e.g. `WebsocketServer(13254, host='0.0.0.0')`. ###Properties From 0fc5156780e9ef086dc155cddf78ee534e0f128c Mon Sep 17 00:00:00 2001 From: Johan Date: Thu, 12 Jan 2017 14:27:21 +0000 Subject: [PATCH 010/141] Update README.md Make it clear that the pip version is not up-to-date --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9bedcae..282a468 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Then just open `client.html` in your browser and you should be able to send and Using in your project ======================= -You can either simply copy/paste the *websocket_server.py* file in your project and use it directly -or you can install the project directly from PyPi: +You can either simply copy/paste the *websocket_server.py* file in your project and use it directly (recommended) +or you can install the project directly from PyPi (might not be up-to-date): pip install websocket-server From 1d8f81fa45935e9f4661ff7548abfae83a26c9f4 Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 13:52:50 +0000 Subject: [PATCH 011/141] Update testsuite --- tests/README.md | 8 ++++++++ tests/_bootstrap_.py | 2 +- tests/message_lengths.py | 12 ++++++------ 3 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 tests/README.md diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..489f352 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,8 @@ +Testing +-------- + +Run server + + python message_lengths.py + +Open client.html in the browser and refresh consequently until all test cases pass. diff --git a/tests/_bootstrap_.py b/tests/_bootstrap_.py index 269a495..9294c46 100644 --- a/tests/_bootstrap_.py +++ b/tests/_bootstrap_.py @@ -1,4 +1,4 @@ #Bootstrap import sys, os -if 'python-websockets-server' in os.getcwd(): +if 'websocket-server' in os.getcwd(): sys.path.insert(0, '..') diff --git a/tests/message_lengths.py b/tests/message_lengths.py index 079d1c1..8ed590a 100644 --- a/tests/message_lengths.py +++ b/tests/message_lengths.py @@ -1,11 +1,11 @@ import _bootstrap_ -from websocket import WebSocketsServer +from websocket_server import WebsocketServer from time import sleep from testsuite.messages import * ''' -This creates just a server that will send a different message to every new connection: - +This creates just a server that will send a different message to every new connection: + 1. A message of length less than 126 2. A message of length 126 3. A message of length 127 @@ -13,8 +13,8 @@ 5. A message above 1024 6. A message above 65K 7. An enormous message (well beyond 65K) - - + + Reconnect to get the next message ''' @@ -52,6 +52,6 @@ def new_client(client, server): PORT=9001 -server = WebSocketsServer(PORT) +server = WebsocketServer(PORT) server.set_fn_new_client(new_client) server.run_forever() From 483b63582a684d1057240a81d8cd235e97fab95e Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 13:53:04 +0000 Subject: [PATCH 012/141] Don't track caching files --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From 0ee51dba8c22a5107f5eca305806fdcc07d5d9ca Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 15:37:50 +0000 Subject: [PATCH 013/141] Fix whitespaces and other stilistic changes --- websocket_server/websocket_server.py | 518 ++++++++++++++------------- 1 file changed, 260 insertions(+), 258 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 53313cb..f77ed90 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -1,18 +1,19 @@ # Author: Johan Hanssen Seferidis # License: MIT -import re, sys +import re +import sys import struct from base64 import b64encode from hashlib import sha1 import logging -if sys.version_info[0] < 3 : - from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler +if sys.version_info[0] < 3: + from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler else: - from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler - + from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler +logger = logging.getLogger(__name__) ''' @@ -45,290 +46,291 @@ OPCODE_PING = 0x9 OPCODE_PONG = 0xA -# ------------------------------ Logging ------------------------------- -logger = logging.getLogger(__name__) # -------------------------------- API --------------------------------- class API(): - def run_forever(self): - try: - logger.info("Listening on port %d for clients.." % self.port) - self.serve_forever() - except KeyboardInterrupt: - self.server_close() - logger.info("Server terminated.") - except Exception as e: - logger.error("ERROR: WebSocketsServer: " + str(e), exc_info=True) - exit(1) - def new_client(self, client, server): - pass - def client_left(self, client, server): - pass - def message_received(self, client, server, message): - pass - def set_fn_new_client(self, fn): - self.new_client=fn - def set_fn_client_left(self, fn): - self.client_left=fn - def set_fn_message_received(self, fn): - self.message_received=fn - def send_message(self, client, msg): - self._unicast_(client, msg) - def send_message_to_all(self, msg): - self._multicast_(msg) + def run_forever(self): + try: + logger.info("Listening on port %d for clients.." % self.port) + self.serve_forever() + except KeyboardInterrupt: + self.server_close() + logger.info("Server terminated.") + except Exception as e: + logger.error("ERROR: WebSocketsServer: " + str(e), exc_info=True) + exit(1) + + def new_client(self, client, server): + pass + + def client_left(self, client, server): + pass + + def message_received(self, client, server, message): + pass + + def set_fn_new_client(self, fn): + self.new_client = fn + + def set_fn_client_left(self, fn): + self.client_left = fn + + def set_fn_message_received(self, fn): + self.message_received = fn + + def send_message(self, client, msg): + self._unicast_(client, msg) + + def send_message_to_all(self, msg): + self._multicast_(msg) # ------------------------- Implementation ----------------------------- class WebsocketServer(ThreadingMixIn, TCPServer, API): - allow_reuse_address = True - daemon_threads = True # comment to keep threads alive until finished - - ''' - clients is a list of dict: - { - 'id' : id, - 'handler' : handler, - 'address' : (addr, port) - } - ''' - clients=[] - id_counter=0 + allow_reuse_address = True + daemon_threads = True # comment to keep threads alive until finished - def __init__(self, port, host='127.0.0.1'): - self.port=port - TCPServer.__init__(self, (host, port), WebSocketHandler) + ''' + clients is a list of dict: + { + 'id' : id, + 'handler' : handler, + 'address' : (addr, port) + } + ''' + clients=[] + id_counter=0 - def _message_received_(self, handler, msg): - self.message_received(self.handler_to_client(handler), self, msg) + def __init__(self, port, host='127.0.0.1'): + self.port=port + TCPServer.__init__(self, (host, port), WebSocketHandler) - def _ping_received_(self, handler, msg): - handler.send_pong(msg) + def _message_received_(self, handler, msg): + self.message_received(self.handler_to_client(handler), self, msg) - def _pong_received_(self, handler, msg): - pass + def _ping_received_(self, handler, msg): + handler.send_pong(msg) - def _new_client_(self, handler): - self.id_counter += 1 - client={ - 'id' : self.id_counter, - 'handler' : handler, - 'address' : handler.client_address - } - self.clients.append(client) - self.new_client(client, self) + def _pong_received_(self, handler, msg): + pass - def _client_left_(self, handler): - client=self.handler_to_client(handler) - self.client_left(client, self) - if client in self.clients: - self.clients.remove(client) + def _new_client_(self, handler): + self.id_counter += 1 + client = { + 'id': self.id_counter, + 'handler': handler, + 'address': handler.client_address + } + self.clients.append(client) + self.new_client(client, self) - def _unicast_(self, to_client, msg): - to_client['handler'].send_message(msg) + def _client_left_(self, handler): + client = self.handler_to_client(handler) + self.client_left(client, self) + if client in self.clients: + self.clients.remove(client) - def _multicast_(self, msg): - for client in self.clients: - self._unicast_(client, msg) + def _unicast_(self, to_client, msg): + to_client['handler'].send_message(msg) - def handler_to_client(self, handler): - for client in self.clients: - if client['handler'] == handler: - return client + def _multicast_(self, msg): + for client in self.clients: + self._unicast_(client, msg) + def handler_to_client(self, handler): + for client in self.clients: + if client['handler'] == handler: + return client class WebSocketHandler(StreamRequestHandler): - def __init__(self, socket, addr, server): - self.server=server - StreamRequestHandler.__init__(self, socket, addr, server) - - def setup(self): - StreamRequestHandler.setup(self) - self.keep_alive = True - self.handshake_done = False - self.valid_client = False - - def handle(self): - while self.keep_alive: - if not self.handshake_done: - self.handshake() - elif self.valid_client: - self.read_next_message() - - def read_bytes(self, num): - # python3 gives ordinal of byte directly - bytes = self.rfile.read(num) - if sys.version_info[0] < 3: - return map(ord, bytes) - else: - return bytes - - def read_next_message(self): - try: - b1, b2 = self.read_bytes(2) - except ValueError as e: - b1, b2 = 0, 0 - - fin = b1 & FIN - opcode = b1 & OPCODE - masked = b2 & MASKED - payload_length = b2 & PAYLOAD_LEN - - if not b1: - logger.info("Client closed connection.") - self.keep_alive = 0 - return - if opcode == OPCODE_CLOSE_CONN: - logger.info("Client asked to close connection.") - self.keep_alive = 0 - return - if not masked: - logger.warn("Client must always be masked.") - self.keep_alive = 0 - return - if opcode == OPCODE_CONTINUATION: - logger.warn("Continuation frames are not supported.") - return - elif opcode == OPCODE_BINARY: - logger.warn("Binary frames are not supported.") - return - elif opcode == OPCODE_TEXT: - opcode_handler = self.server._message_received_ - elif opcode == OPCODE_PING: - opcode_handler = self.server._ping_received_ - elif opcode == OPCODE_PONG: - opcode_handler = self.server._pong_received_ - else: - logger.warn("Unknown opcode %#x." + opcode) - self.keep_alive = 0 - return - - if payload_length == 126: - payload_length = struct.unpack(">H", self.rfile.read(2))[0] - elif payload_length == 127: - payload_length = struct.unpack(">Q", self.rfile.read(8))[0] - - masks = self.read_bytes(4) - decoded = "" - for char in self.read_bytes(payload_length): - char ^= masks[len(decoded) % 4] - decoded += chr(char) - opcode_handler(self, decoded) - - def send_message(self, message): - self.send_text(message) - - def send_pong(self, message): - self.send_text(message, OPCODE_PONG) - - def send_text(self, message, opcode=OPCODE_TEXT): - ''' - NOTES - Fragmented(=continuation) messages are not being used since their usage - is needed in very limited cases - when we don't know the payload length. - ''' - - # Validate message - if isinstance(message, bytes): - message = try_decode_UTF8(message) # this is slower but assures we have UTF-8 - if not message: - logger.warning("Can\'t send message, message is not valid UTF-8") - return False - elif isinstance(message, str) or isinstance(message, unicode): - pass - else: - logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) - return False - - header = bytearray() - payload = encode_to_UTF8(message) - payload_length = len(payload) - - # Normal payload - if payload_length <= 125: - header.append(FIN | opcode) - header.append(payload_length) - - # Extended payload - elif payload_length >= 126 and payload_length <= 65535: - header.append(FIN | opcode) - header.append(PAYLOAD_LEN_EXT16) - header.extend(struct.pack(">H", payload_length)) - - # Huge extended payload - elif payload_length < 18446744073709551616: - header.append(FIN | opcode) - header.append(PAYLOAD_LEN_EXT64) - header.extend(struct.pack(">Q", payload_length)) - - else: - raise Exception("Message is too big. Consider breaking it into chunks.") - return - - self.request.send(header + payload) - - def handshake(self): - message = self.request.recv(1024).decode().strip() - upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) - if not upgrade: - self.keep_alive = False - return - key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) - if key: - key = key.group(1) - else: - logger.warning("Client tried to connect but was missing a key") - self.keep_alive = False - return - response = self.make_handshake_response(key) - self.handshake_done = self.request.send(response.encode()) - self.valid_client = True - self.server._new_client_(self) - - def make_handshake_response(self, key): - return \ - 'HTTP/1.1 101 Switching Protocols\r\n'\ - 'Upgrade: websocket\r\n' \ - 'Connection: Upgrade\r\n' \ - 'Sec-WebSocket-Accept: %s\r\n' \ - '\r\n' % self.calculate_response_key(key) - - def calculate_response_key(self, key): - GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' - hash = sha1(key.encode() + GUID.encode()) - response_key = b64encode(hash.digest()).strip() - return response_key.decode('ASCII') - - def finish(self): - self.server._client_left_(self) - + def __init__(self, socket, addr, server): + self.server = server + StreamRequestHandler.__init__(self, socket, addr, server) + + def setup(self): + StreamRequestHandler.setup(self) + self.keep_alive = True + self.handshake_done = False + self.valid_client = False + + def handle(self): + while self.keep_alive: + if not self.handshake_done: + self.handshake() + elif self.valid_client: + self.read_next_message() + + def read_bytes(self, num): + # python3 gives ordinal of byte directly + bytes = self.rfile.read(num) + if sys.version_info[0] < 3: + return map(ord, bytes) + else: + return bytes + + def read_next_message(self): + try: + b1, b2 = self.read_bytes(2) + except ValueError as e: + b1, b2 = 0, 0 + + fin = b1 & FIN + opcode = b1 & OPCODE + masked = b2 & MASKED + payload_length = b2 & PAYLOAD_LEN + + if not b1: + logger.info("Client closed connection.") + self.keep_alive = 0 + return + if opcode == OPCODE_CLOSE_CONN: + logger.info("Client asked to close connection.") + self.keep_alive = 0 + return + if not masked: + logger.warn("Client must always be masked.") + self.keep_alive = 0 + return + if opcode == OPCODE_CONTINUATION: + logger.warn("Continuation frames are not supported.") + return + elif opcode == OPCODE_BINARY: + logger.warn("Binary frames are not supported.") + return + elif opcode == OPCODE_TEXT: + opcode_handler = self.server._message_received_ + elif opcode == OPCODE_PING: + opcode_handler = self.server._ping_received_ + elif opcode == OPCODE_PONG: + opcode_handler = self.server._pong_received_ + else: + logger.warn("Unknown opcode %#x." + opcode) + self.keep_alive = 0 + return + + if payload_length == 126: + payload_length = struct.unpack(">H", self.rfile.read(2))[0] + elif payload_length == 127: + payload_length = struct.unpack(">Q", self.rfile.read(8))[0] + + masks = self.read_bytes(4) + decoded = "" + for char in self.read_bytes(payload_length): + char ^= masks[len(decoded) % 4] + decoded += chr(char) + opcode_handler(self, decoded) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, OPCODE_PONG) + + def send_text(self, message, opcode=OPCODE_TEXT): + """ + Important: Fragmented(=continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = try_decode_UTF8(message) # this is slower but ensures we have UTF-8 + if not message: + logger.warning("Can\'t send message, message is not valid UTF-8") + return False + elif isinstance(message, str) or isinstance(message, unicode): + pass + else: + logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + return False + + header = bytearray() + payload = encode_to_UTF8(message) + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < 18446744073709551616: + header.append(FIN | opcode) + header.append(PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big. Consider breaking it into chunks.") + return + + self.request.send(header + payload) + + def handshake(self): + message = self.request.recv(1024).decode().strip() + upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) + if not upgrade: + self.keep_alive = False + return + key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) + if key: + key = key.group(1) + else: + logger.warning("Client tried to connect but was missing a key") + self.keep_alive = False + return + response = self.make_handshake_response(key) + self.handshake_done = self.request.send(response.encode()) + self.valid_client = True + self.server._new_client_(self) + + def make_handshake_response(self, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % self.calculate_response_key(key) + + def calculate_response_key(self, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.server._client_left_(self) def encode_to_UTF8(data): - try: - return data.encode('UTF-8') - except UnicodeEncodeError as e: - logger.error("Could not encode data to UTF-8 -- %s" % e) - return False - except Exception as e: - raise(e) - return False - + try: + return data.encode('UTF-8') + except UnicodeEncodeError as e: + logger.error("Could not encode data to UTF-8 -- %s" % e) + return False + except Exception as e: + raise(e) + return False def try_decode_UTF8(data): - try: - return data.decode('utf-8') - except UnicodeDecodeError: - return False - except Exception as e: - raise(e) - + try: + return data.decode('utf-8') + except UnicodeDecodeError: + return False + except Exception as e: + raise(e) # This is only for testing purposes From e582a6a8d414a1277fc5542ad91bf74d3de7f301 Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 15:46:10 +0000 Subject: [PATCH 014/141] Add files to ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d20b64..d9a3498 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +.cache From 04ac4c99fd3703f898a8dd5c068900a94bbd150a Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 15:47:00 +0000 Subject: [PATCH 015/141] Clean up testing and make unit tests to work with pytest --- tests/_bootstrap_.py | 4 ++- tests/handshake.py | 40 ---------------------------- tests/test_handshake.py | 30 +++++++++++++++++++++ websocket_server/websocket_server.py | 6 ----- 4 files changed, 33 insertions(+), 47 deletions(-) delete mode 100644 tests/handshake.py create mode 100644 tests/test_handshake.py diff --git a/tests/_bootstrap_.py b/tests/_bootstrap_.py index 9294c46..6fd0248 100644 --- a/tests/_bootstrap_.py +++ b/tests/_bootstrap_.py @@ -1,4 +1,6 @@ #Bootstrap import sys, os -if 'websocket-server' in os.getcwd(): +if os.getcwd().endswith('tests'): sys.path.insert(0, '..') +elif os.getcwd().endswith('websocket-server'): + sys.path.insert(0, '.') diff --git a/tests/handshake.py b/tests/handshake.py deleted file mode 100644 index 1ebfe1d..0000000 --- a/tests/handshake.py +++ /dev/null @@ -1,40 +0,0 @@ -import _bootstrap_ -from websocket import * - - -handler = DummyWebsocketHandler() - - - - -pairs = [ - # Key # Response - ('zyjFH2rQwrTtNFk5lwEMQg==', '2hnZADGmT/V1/w1GJYBtttUKASY='), - ('XJuxlsdq0QrVyKwA/D9D5A==', 'tZ5RV3pw7nP9cF+HDvTd89WJKj8=') -] - - -# Test hash calculations for response -key = 'zyjFH2rQwrTtNFk5lwEMQg==' -resp = handler.calculate_response_key(key) -assert resp == '2hnZADGmT/V1/w1GJYBtttUKASY=' - - -# Test response messages -key = 'zyjFH2rQwrTtNFk5lwEMQg==' -expect = \ - 'HTTP/1.1 101 Switching Protocols\r\n'\ - 'Upgrade: websocket\r\n' \ - 'Connection: Upgrade\r\n' \ - 'Sec-WebSocket-Accept: 2hnZADGmT/V1/w1GJYBtttUKASY=\r\n'\ - '\r\n' -resp = handler.make_handshake_response(key) -assert resp == expect - - - - - - - -print("No errors") diff --git a/tests/test_handshake.py b/tests/test_handshake.py new file mode 100644 index 0000000..c3dcaf7 --- /dev/null +++ b/tests/test_handshake.py @@ -0,0 +1,30 @@ +import _bootstrap_ +from websocket_server import * + +class DummyWebsocketHandler(WebSocketHandler): + def __init__(self, *_): + pass + +handler = DummyWebsocketHandler() + +pairs = [ + # Key # Response + ('zyjFH2rQwrTtNFk5lwEMQg==', '2hnZADGmT/V1/w1GJYBtttUKASY='), + ('XJuxlsdq0QrVyKwA/D9D5A==', 'tZ5RV3pw7nP9cF+HDvTd89WJKj8=') +] + +def test_hash_calculations_for_response(): + key = 'zyjFH2rQwrTtNFk5lwEMQg==' + resp = handler.calculate_response_key(key) + assert resp == '2hnZADGmT/V1/w1GJYBtttUKASY=' + +def test_response_messages(): + key = 'zyjFH2rQwrTtNFk5lwEMQg==' + expect = \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: 2hnZADGmT/V1/w1GJYBtttUKASY=\r\n'\ + '\r\n' + resp = handler.make_handshake_response(key) + assert resp == expect diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index f77ed90..b5e8dea 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -331,9 +331,3 @@ def try_decode_UTF8(data): return False except Exception as e: raise(e) - - -# This is only for testing purposes -class DummyWebsocketHandler(WebSocketHandler): - def __init__(self, *_): - pass From 282c56dd964b09d8a20ca20548bcc8cd96e2a37d Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 15:52:09 +0000 Subject: [PATCH 016/141] Refactor tests --- tests/README.md | 7 ++++++- tests/test_handshake.py | 27 +++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/README.md b/tests/README.md index 489f352..810d55e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,7 +1,12 @@ Testing -------- -Run server +Run unit tests + + pytest + + +Run functional tests python message_lengths.py diff --git a/tests/test_handshake.py b/tests/test_handshake.py index c3dcaf7..2aa2e96 100644 --- a/tests/test_handshake.py +++ b/tests/test_handshake.py @@ -1,30 +1,29 @@ import _bootstrap_ from websocket_server import * +import pytest + class DummyWebsocketHandler(WebSocketHandler): def __init__(self, *_): pass -handler = DummyWebsocketHandler() - -pairs = [ - # Key # Response - ('zyjFH2rQwrTtNFk5lwEMQg==', '2hnZADGmT/V1/w1GJYBtttUKASY='), - ('XJuxlsdq0QrVyKwA/D9D5A==', 'tZ5RV3pw7nP9cF+HDvTd89WJKj8=') -] +@pytest.fixture +def websocket_handler(): + return DummyWebsocketHandler() -def test_hash_calculations_for_response(): +def test_hash_calculations_for_response(websocket_handler): key = 'zyjFH2rQwrTtNFk5lwEMQg==' - resp = handler.calculate_response_key(key) - assert resp == '2hnZADGmT/V1/w1GJYBtttUKASY=' + expected_key = '2hnZADGmT/V1/w1GJYBtttUKASY=' + assert websocket_handler.calculate_response_key(key) == expected_key + -def test_response_messages(): +def test_response_messages(websocket_handler): key = 'zyjFH2rQwrTtNFk5lwEMQg==' - expect = \ + expected = \ 'HTTP/1.1 101 Switching Protocols\r\n'\ 'Upgrade: websocket\r\n' \ 'Connection: Upgrade\r\n' \ 'Sec-WebSocket-Accept: 2hnZADGmT/V1/w1GJYBtttUKASY=\r\n'\ '\r\n' - resp = handler.make_handshake_response(key) - assert resp == expect + handshake_content = websocket_handler.make_handshake_response(key) + assert handshake_content == expected From dc749e3bc8ffa1a8a6fcc6ea859b6d64af8dc0fd Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 16:00:07 +0000 Subject: [PATCH 017/141] Update installation instructions --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 282a468..0afc41a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A minimal Websockets Server in Python with no external dependencies. * Clean simple API * Multiple clients * No dependencies - + Notice that this implementation does not support the more advanced features like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. @@ -18,16 +18,18 @@ Usage You can get a feel of how to use the websocket server by running python server.py - + Then just open `client.html` in your browser and you should be able to send and receive messages. Using in your project ======================= -You can either simply copy/paste the *websocket_server.py* file in your project and use it directly (recommended) -or you can install the project directly from PyPi (might not be up-to-date): - pip install websocket-server +You can use the project in three ways. + + 1. Copy/paste the *websocket_server.py* file in your project and use it directly + 2. `pip install git://github.com/Pithikos/python-websocket-server` (latest code) + 3. `pip install websocket-server` (might not be up-to-date) For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api). @@ -97,4 +99,3 @@ Client is just a dictionary passed along methods. 'address' : (addr, port) } ```` - From 4815a08794bb999a8006b1c088304bb4bee0c1b9 Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 16:17:40 +0000 Subject: [PATCH 018/141] Better error output --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index b5e8dea..7e63472 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -59,7 +59,7 @@ def run_forever(self): self.server_close() logger.info("Server terminated.") except Exception as e: - logger.error("ERROR: WebSocketsServer: " + str(e), exc_info=True) + logger.error(str(e), exc_info=True) exit(1) def new_client(self, client, server): From eb93f50bbd39d54551ff1844f3fc397779ffef92 Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 17:16:07 +0000 Subject: [PATCH 019/141] Allow logging configuration directly via API --- README.md | 17 +++++++++++------ websocket_server/websocket_server.py | 7 ++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0afc41a..f86d767 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,14 @@ The API is simply methods and properties of the `WebsocketServer` class. ## WebsocketServer -The WebsocketServer takes two arguments: a `port` and a `hostname`. -By default the localhost `127.0.0.1` is used. However if you want to be able and connect -to the server from the network you need to pass `0.0.0.0` as hostname e.g. `WebsocketServer(13254, host='0.0.0.0')`. +The WebsocketServer can be initialized with the below parameters. + +*`port`* - The port clients will need to connect to. + +*`host`* - By default the `127.0.0.1` is used which allows connections only from the current machine. If you wish to allow all network machines to connect, you need to pass `0.0.0.0` as hostname. + +*`loglevel`* - logging level to print. By default WARNING is used. You can use `logging.DEBUG` or `logging.INFO` for more verbose output. + ###Properties @@ -72,18 +77,18 @@ to the server from the network you need to pass `0.0.0.0` as hostname e.g. `Webs | `set_fn_message_received()` | Called when a `client` sends a `message` | client, server, message | -The client passed to the callback is the client that left, sent the message, etc. The server might not have any use to use. However it is -passed in case you want to send messages to clients. +The client passed to the callback is the client that left, sent the message, etc. The server might not have any use to use. However it is passed in case you want to send messages to clients. Example: ```` +import logging from websocket_server import WebsocketServer def new_client(client, server): server.send_message_to_all("Hey all, a new client has joined us") -server = WebsocketServer(13254, host='127.0.0.1') +server = WebsocketServer(13254, host='127.0.0.1', loglevel=logging.INFO) server.set_fn_new_client(new_client) server.run_forever() ```` diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 7e63472..733854b 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -14,7 +14,7 @@ from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler logger = logging.getLogger(__name__) - +logging.basicConfig() ''' +-+-+-+-+-------+-+-------------+-------------------------------+ @@ -105,8 +105,9 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): clients=[] id_counter=0 - def __init__(self, port, host='127.0.0.1'): - self.port=port + def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): + logger.setLevel(loglevel) + self.port = port TCPServer.__init__(self, (host, port), WebSocketHandler) def _message_received_(self, handler, msg): From 0ab71f099c1afec1c10e8ec4113b802812dbec4c Mon Sep 17 00:00:00 2001 From: Pithikos Date: Wed, 15 Mar 2017 17:25:15 +0000 Subject: [PATCH 020/141] Fix invalid markdown --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f86d767..22d4b9b 100644 --- a/README.md +++ b/README.md @@ -50,14 +50,14 @@ The WebsocketServer can be initialized with the below parameters. *`loglevel`* - logging level to print. By default WARNING is used. You can use `logging.DEBUG` or `logging.INFO` for more verbose output. -###Properties +### Properties | Property | Description | |----------|----------------------| | clients | A list of `client` | -###Methods +### Methods | Method | Description | Takes | Gives | |-----------------------------|---------------------------------------------------------------------------------------|-----------------|-------| @@ -68,7 +68,7 @@ The WebsocketServer can be initialized with the below parameters. | `send_message_to_all()` | Sends a `message` to **all** connected clients. The message is a simple string. | message | None | -###Callback functions +### Callback functions | Set by | Description | Parameters | |-----------------------------|---------------------------------------------------|-------------------------| @@ -93,7 +93,7 @@ server.set_fn_new_client(new_client) server.run_forever() ```` -##Client +## Client Client is just a dictionary passed along methods. From c257b0f1205a43728c58be8de0992f45671d6071 Mon Sep 17 00:00:00 2001 From: Pithikos Date: Thu, 16 Mar 2017 10:00:25 +0000 Subject: [PATCH 021/141] Further documentation and small stilistic changes --- websocket_server/websocket_server.py | 36 ++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 733854b..dd0af82 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -90,20 +90,32 @@ def send_message_to_all(self, msg): # ------------------------- Implementation ----------------------------- class WebsocketServer(ThreadingMixIn, TCPServer, API): + """ + A websocket server waiting for clients to connect. + + Args: + port(int): Port to bind to + host(str): Hostname or IP to listen for connections. By default 127.0.0.1 + is being used. To accept connections from any client, you should use + 0.0.0.0. + loglevel: Logging level from logging module to use for logging. By default + warnings and errors are being logged. + + Properties: + clients(list): A list of connected clients. A client is a dictionary + like below. + { + 'id' : id, + 'handler' : handler, + 'address' : (addr, port) + } + """ allow_reuse_address = True - daemon_threads = True # comment to keep threads alive until finished - - ''' - clients is a list of dict: - { - 'id' : id, - 'handler' : handler, - 'address' : (addr, port) - } - ''' - clients=[] - id_counter=0 + daemon_threads = True # comment to keep threads alive until finished + + clients = [] + id_counter = 0 def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): logger.setLevel(loglevel) From 46d2ed4d402e2076583e6b8686eb2ea9e284847e Mon Sep 17 00:00:00 2001 From: Pithikos Date: Thu, 16 Mar 2017 11:25:57 +0000 Subject: [PATCH 022/141] Unifinished test refactoring --- tests/README.md | 5 +++ tests/test_message_lengths.py | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/test_message_lengths.py diff --git a/tests/README.md b/tests/README.md index 810d55e..41f684d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,6 +1,11 @@ Testing -------- +Install prerequisites + + pip install pytest websocket-client + + Run unit tests pytest diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py new file mode 100644 index 0000000..b6e5737 --- /dev/null +++ b/tests/test_message_lengths.py @@ -0,0 +1,68 @@ +from time import sleep +import logging +from threading import Thread + +import _bootstrap_ +from websocket_server import WebsocketServer +from testsuite.messages import * + +from websocket import create_connection + +''' +This creates just a server that will send a different message to every new connection: + + 1. A message of length less than 126 + 2. A message of length 126 + 3. A message of length 127 + 4. A message of length bigger than 127 + 5. A message above 1024 + 6. A message above 65K + 7. An enormous message (well beyond 65K) + +Reconnect to get the next message +''' + + +counter = 0 + +# Called for every client connecting (after handshake) +def new_client(client, server): + print("New client connected and was given id %d" % client['id']) + global counter + if counter == 0: + print("Sending message 1 of length %d" % len(msg_125B)) + server.send_message(client, msg_125B) + elif counter == 1: + print("Sending message 2 of length %d" % len(msg_126B)) + server.send_message(client, msg_126B) + elif counter == 2: + print("Sending message 3 of length %d" % len(msg_127B)) + server.send_message(client, msg_127B) + elif counter == 3: + print("Sending message 4 of length %d" % len(msg_208B)) + server.send_message(client, msg_208B) + elif counter == 4: + print("Sending message 5 of length %d" % len(msg_1251B)) + server.send_message(client, msg_1251B) + elif counter == 5: + print("Sending message 6 of length %d" % len(msg_68KB)) + server.send_message(client, msg_68KB) + elif counter == 6: + print("Sending message 7 of length %d" % len(msg_1500KB)) + server.send_message(client, msg_1500KB) + else: + print("No errors") + counter += 1 + + +PORT = 9001 +server = WebsocketServer(PORT, loglevel=logging.DEBUG) +server.set_fn_new_client(new_client) + +server_thread = Thread(target=server.run_forever) + +server_thread.start() + +print('TEST') + +ws = create_connection("ws://localhost:%d" % PORT) From d6343c6132eed13a10dad9ded1fcb38bcbf18d12 Mon Sep 17 00:00:00 2001 From: Manos Date: Mon, 17 Apr 2017 18:31:44 +0100 Subject: [PATCH 023/141] Fix pip installation instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22d4b9b..cf1da37 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Using in your project You can use the project in three ways. 1. Copy/paste the *websocket_server.py* file in your project and use it directly - 2. `pip install git://github.com/Pithikos/python-websocket-server` (latest code) + 2. `pip install git+https://github.com/Pithikos/python-websocket-server` (latest code) 3. `pip install websocket-server` (might not be up-to-date) For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api). From 2ba8a2dd79c4e1a790b8dcd2f0aaf00c3666950c Mon Sep 17 00:00:00 2001 From: Manos Date: Tue, 18 Apr 2017 16:07:52 +0100 Subject: [PATCH 024/141] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index cf1da37..d8c4c1e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,10 @@ Notice that this implementation does not support the more advanced features like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. +I'm out of work. If this project reduced your development time please show your appreciation. + +[![Donate](https://www.paypal.com/en_US/i/btn/x-click-but21.gif)](https://www.paypal.me/seferidis) + Usage ======================= From 11e5bafbe619aa49024da68c298edc062644ff69 Mon Sep 17 00:00:00 2001 From: Manos Date: Mon, 24 Apr 2017 19:25:32 +0100 Subject: [PATCH 025/141] Make README less melodramatic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8c4c1e..823e357 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Notice that this implementation does not support the more advanced features like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. -I'm out of work. If this project reduced your development time please show your appreciation. +If this project reduced your development time please show your appreciation. [![Donate](https://www.paypal.com/en_US/i/btn/x-click-but21.gif)](https://www.paypal.me/seferidis) From 17b6f382ab4d0f30d9e1b2da891694888d1b795b Mon Sep 17 00:00:00 2001 From: Manos Date: Mon, 24 Apr 2017 19:27:28 +0100 Subject: [PATCH 026/141] Make README less melodramatic --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 823e357..c2a2c97 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Notice that this implementation does not support the more advanced features like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. -If this project reduced your development time please show your appreciation. +If this project reduced your development time feel free to buy me a coffee. [![Donate](https://www.paypal.com/en_US/i/btn/x-click-but21.gif)](https://www.paypal.me/seferidis) From 72cd19bc68a50f237f204562743bca2eddf96479 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Thu, 20 Jul 2017 18:13:54 +0200 Subject: [PATCH 027/141] added syntax highlighting --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c2a2c97..02873d4 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ The client passed to the callback is the client that left, sent the message, etc Example: -```` +````py import logging from websocket_server import WebsocketServer @@ -101,10 +101,10 @@ server.run_forever() Client is just a dictionary passed along methods. -```` +```py { 'id' : client_id, 'handler' : client_handler, 'address' : (addr, port) } -```` +``` From b4c3fe21dc6c043791c09a0f833c622630c2b70d Mon Sep 17 00:00:00 2001 From: Manos Date: Sat, 28 Oct 2017 10:28:48 +0100 Subject: [PATCH 028/141] Update README.md Remove donation button --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index c2a2c97..cf1da37 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,6 @@ Notice that this implementation does not support the more advanced features like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. -If this project reduced your development time feel free to buy me a coffee. - -[![Donate](https://www.paypal.com/en_US/i/btn/x-click-but21.gif)](https://www.paypal.me/seferidis) - Usage ======================= From f4a5aa5f313caeb7f8fbe2e0295ba79d5c80fefa Mon Sep 17 00:00:00 2001 From: Max Mikhaylov Date: Fri, 17 Nov 2017 12:07:42 -0500 Subject: [PATCH 029/141] Fixed message validation for Python 3 Added short-circuit that prevents checking if message is `unicode`, if Python version is 3.0 or higher. There is no `unicode` keyword in Python 3 since all strings are sequences of Unicode characters. --- websocket_server/websocket_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index dd0af82..fc50b8e 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -257,7 +257,9 @@ def send_text(self, message, opcode=OPCODE_TEXT): if not message: logger.warning("Can\'t send message, message is not valid UTF-8") return False - elif isinstance(message, str) or isinstance(message, unicode): + elif sys.version_info < (3,0) and (isinstance(message, str) or isinstance(message, unicode)): + pass + elif isinstance(message, str): pass else: logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) From 0d68418bdb7f0d6b33db0d1a2d77c19d3ac2965e Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Sun, 28 Jan 2018 00:40:31 +0000 Subject: [PATCH 030/141] Add license --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf4494c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Johan Hanssen Seferidis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From a2d86006c358d719db78636e13e741769240f9a3 Mon Sep 17 00:00:00 2001 From: Manos Date: Sun, 28 Jan 2018 00:44:17 +0000 Subject: [PATCH 031/141] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index bf4494c..db7febd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Johan Hanssen Seferidis +Copyright (c) 2018 Johan Hanssen Seferidis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From eb69534bb6ce6c70c21e8b33f6050546d49a2c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E7=84=B0?= Date: Thu, 22 Feb 2018 21:44:52 +0800 Subject: [PATCH 032/141] fix:read_next_message() when recv chinese we can't decode message form payload byte by byte. some payload like Chinese word encode by utf-8 need 3 bytes to indicate one char. so, collect all bytes from payload and decode them at one time may be better. --- websocket_server/websocket_server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index fc50b8e..10eef40 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -233,11 +233,11 @@ def read_next_message(self): payload_length = struct.unpack(">Q", self.rfile.read(8))[0] masks = self.read_bytes(4) - decoded = "" - for char in self.read_bytes(payload_length): - char ^= masks[len(decoded) % 4] - decoded += chr(char) - opcode_handler(self, decoded) + message_bytes = bytearray() + for message_byte in self.read_bytes(payload_length): + message_byte ^= masks[len(message_bytes) % 4] + message_bytes.append(message_byte) + opcode_handler(self, message_bytes.decode('utf8')) def send_message(self, message): self.send_text(message) From 04e32bf5b0d2daff712191e86771c3a70b4c7cd0 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 17:15:49 +0000 Subject: [PATCH 033/141] Server port now will be the actual listening port (even in case of 0) --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index dd0af82..ece8ba0 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -119,8 +119,8 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): logger.setLevel(loglevel) - self.port = port TCPServer.__init__(self, (host, port), WebSocketHandler) + self.port = self.socket.getsockname()[1] def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) From 7089c983b158615bb81d9a41edcaa29431e59a8d Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 18:07:45 +0000 Subject: [PATCH 034/141] Fully automate tests --- tests/README.md | 3 +- tests/_bootstrap_.py | 2 +- tests/requirements.txt | 2 + tests/test_handshake.py | 3 + tests/test_message_lengths.py | 152 ++++++++++++++++++++-------------- tests/testsuite/__init__.py | 0 tests/testsuite/messages.py | 21 ----- 7 files changed, 98 insertions(+), 85 deletions(-) create mode 100644 tests/requirements.txt delete mode 100644 tests/testsuite/__init__.py delete mode 100644 tests/testsuite/messages.py diff --git a/tests/README.md b/tests/README.md index 41f684d..b6c8e45 100644 --- a/tests/README.md +++ b/tests/README.md @@ -3,7 +3,8 @@ Testing Install prerequisites - pip install pytest websocket-client + virtualenv .env -p python3 --clear && . .env/bin/activate + pip install -r tests/requirements.txt Run unit tests diff --git a/tests/_bootstrap_.py b/tests/_bootstrap_.py index 6fd0248..b8a7949 100644 --- a/tests/_bootstrap_.py +++ b/tests/_bootstrap_.py @@ -1,4 +1,4 @@ -#Bootstrap +# Add path to source code import sys, os if os.getcwd().endswith('tests'): sys.path.insert(0, '..') diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 0000000..63fde45 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,2 @@ +pytest +websocket-client diff --git a/tests/test_handshake.py b/tests/test_handshake.py index 2aa2e96..eadf12d 100644 --- a/tests/test_handshake.py +++ b/tests/test_handshake.py @@ -1,5 +1,6 @@ import _bootstrap_ from websocket_server import * + import pytest @@ -7,10 +8,12 @@ class DummyWebsocketHandler(WebSocketHandler): def __init__(self, *_): pass + @pytest.fixture def websocket_handler(): return DummyWebsocketHandler() + def test_hash_calculations_for_response(websocket_handler): key = 'zyjFH2rQwrTtNFk5lwEMQg==' expected_key = '2hnZADGmT/V1/w1GJYBtttUKASY=' diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py index b6e5737..14fec70 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_message_lengths.py @@ -2,67 +2,95 @@ import logging from threading import Thread +import pytest +from websocket import create_connection # websocket-client + import _bootstrap_ from websocket_server import WebsocketServer -from testsuite.messages import * - -from websocket import create_connection - -''' -This creates just a server that will send a different message to every new connection: - - 1. A message of length less than 126 - 2. A message of length 126 - 3. A message of length 127 - 4. A message of length bigger than 127 - 5. A message above 1024 - 6. A message above 65K - 7. An enormous message (well beyond 65K) - -Reconnect to get the next message -''' - - -counter = 0 - -# Called for every client connecting (after handshake) -def new_client(client, server): - print("New client connected and was given id %d" % client['id']) - global counter - if counter == 0: - print("Sending message 1 of length %d" % len(msg_125B)) - server.send_message(client, msg_125B) - elif counter == 1: - print("Sending message 2 of length %d" % len(msg_126B)) - server.send_message(client, msg_126B) - elif counter == 2: - print("Sending message 3 of length %d" % len(msg_127B)) - server.send_message(client, msg_127B) - elif counter == 3: - print("Sending message 4 of length %d" % len(msg_208B)) - server.send_message(client, msg_208B) - elif counter == 4: - print("Sending message 5 of length %d" % len(msg_1251B)) - server.send_message(client, msg_1251B) - elif counter == 5: - print("Sending message 6 of length %d" % len(msg_68KB)) - server.send_message(client, msg_68KB) - elif counter == 6: - print("Sending message 7 of length %d" % len(msg_1500KB)) - server.send_message(client, msg_1500KB) - else: - print("No errors") - counter += 1 - - -PORT = 9001 -server = WebsocketServer(PORT, loglevel=logging.DEBUG) -server.set_fn_new_client(new_client) - -server_thread = Thread(target=server.run_forever) - -server_thread.start() - -print('TEST') - -ws = create_connection("ws://localhost:%d" % PORT) + + +@pytest.fixture(scope='function') +def server(): + """ Returns the response of a server after""" + s = WebsocketServer(0, loglevel=logging.DEBUG) + server_thread = Thread(target=s.run_forever) + server_thread.daemon = True + server_thread.start() + yield s + s.server_close() + + +@pytest.fixture +def session(server): + ws = create_connection("ws://{}:{}".format(*server.server_address)) + yield ws, server + ws.close() + + +def test_text_message_of_length_1(session): + client, server = session + server.send_message_to_all('$') + assert client.recv() == '$' + + +def test_text_message_of_length_125B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqr125' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_126B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrs126' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_127B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrst127' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_208B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw208' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_1251B(session): + client, server = session + msg = ('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqr125'*10)+'1' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_68KB(session): + client, server = session + msg = '$'+('a'*67993)+'68000'+'^' + assert len(msg) == 68000 + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_1500KB(session): + """ An enormous message (well beyond 65K) """ + client, server = session + msg = '$'+('a'*1499991)+'1500000'+'^' + assert len(msg) == 1500000 + server.send_message_to_all(msg) + assert client.recv() == msg diff --git a/tests/testsuite/__init__.py b/tests/testsuite/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/testsuite/messages.py b/tests/testsuite/messages.py deleted file mode 100644 index 5d0a93d..0000000 --- a/tests/testsuite/messages.py +++ /dev/null @@ -1,21 +0,0 @@ -# -# Fixed messages by length -# Every message ends with its length.. -# - -msg_125B = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqr125' -msg_126B = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrs126' -msg_127B = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrst127' -msg_208B = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ - 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw208' -msg_1251B = (msg_125B*10)+'1' # 1251 -msg_68KB = ('a'*67995)+'68000' # 68000 -msg_1500KB = ('a'*1500000)+'1500000' # 1.5Mb From 061a0720e5874d22fb1609d90854a0823e754848 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 18:15:21 +0000 Subject: [PATCH 035/141] Use tox for testing for both Python 2 and 3 --- tests/requirements.txt | 2 -- tox.ini | 6 ++++++ 2 files changed, 6 insertions(+), 2 deletions(-) delete mode 100644 tests/requirements.txt create mode 100644 tox.ini diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 63fde45..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -websocket-client diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b3025e9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py27,py3 +[testenv] +deps=pytest + websocket-client +commands=pytest From bd49305718dc6c372e5ffc126fd2ae9df3518f42 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 18:16:34 +0000 Subject: [PATCH 036/141] Stop tracking garbage --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index d9a3498..dc44a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ *.pyc .cache +.pytest_cache +.tox +.env +*.egg-info From 51e05e3ef0787c2bc0c921ea99fdec0d944372b0 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 18:20:51 +0000 Subject: [PATCH 037/141] Add testing instructions to main README --- README.md | 10 +++++++++- tests/README.md | 19 ------------------- 2 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 tests/README.md diff --git a/README.md b/README.md index 22d4b9b..47c2da3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ You can get a feel of how to use the websocket server by running Then just open `client.html` in your browser and you should be able to send and receive messages. -Using in your project +Installation ======================= You can use the project in three ways. @@ -34,6 +34,14 @@ You can use the project in three ways. For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api). +Testing +======= + +Run all tests + + tox + + API ======================= diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index b6c8e45..0000000 --- a/tests/README.md +++ /dev/null @@ -1,19 +0,0 @@ -Testing --------- - -Install prerequisites - - virtualenv .env -p python3 --clear && . .env/bin/activate - pip install -r tests/requirements.txt - - -Run unit tests - - pytest - - -Run functional tests - - python message_lengths.py - -Open client.html in the browser and refresh consequently until all test cases pass. From c61ca53c226997c4334ef27bdfdf1a22251283f6 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 20:21:44 +0000 Subject: [PATCH 038/141] Convert handshake calculation methods into class methods --- tests/test_handshake.py | 24 +++++------------------- tests/test_message_lengths.py | 28 +--------------------------- tests/utils.py | 26 ++++++++++++++++++++++++++ websocket_server/websocket_server.py | 8 +++++--- 4 files changed, 37 insertions(+), 49 deletions(-) create mode 100644 tests/utils.py diff --git a/tests/test_handshake.py b/tests/test_handshake.py index eadf12d..dfbcc36 100644 --- a/tests/test_handshake.py +++ b/tests/test_handshake.py @@ -1,26 +1,12 @@ import _bootstrap_ -from websocket_server import * +from websocket_server import WebSocketHandler -import pytest +def test_hash_calculations_for_response(): + assert WebSocketHandler.calculate_response_key('zyjFH2rQwrTtNFk5lwEMQg==') == '2hnZADGmT/V1/w1GJYBtttUKASY=' -class DummyWebsocketHandler(WebSocketHandler): - def __init__(self, *_): - pass - -@pytest.fixture -def websocket_handler(): - return DummyWebsocketHandler() - - -def test_hash_calculations_for_response(websocket_handler): - key = 'zyjFH2rQwrTtNFk5lwEMQg==' - expected_key = '2hnZADGmT/V1/w1GJYBtttUKASY=' - assert websocket_handler.calculate_response_key(key) == expected_key - - -def test_response_messages(websocket_handler): +def test_response_messages(): key = 'zyjFH2rQwrTtNFk5lwEMQg==' expected = \ 'HTTP/1.1 101 Switching Protocols\r\n'\ @@ -28,5 +14,5 @@ def test_response_messages(websocket_handler): 'Connection: Upgrade\r\n' \ 'Sec-WebSocket-Accept: 2hnZADGmT/V1/w1GJYBtttUKASY=\r\n'\ '\r\n' - handshake_content = websocket_handler.make_handshake_response(key) + handshake_content = WebSocketHandler.make_handshake_response(key) assert handshake_content == expected diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py index 14fec70..6fe3539 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_message_lengths.py @@ -1,30 +1,4 @@ -from time import sleep -import logging -from threading import Thread - -import pytest -from websocket import create_connection # websocket-client - -import _bootstrap_ -from websocket_server import WebsocketServer - - -@pytest.fixture(scope='function') -def server(): - """ Returns the response of a server after""" - s = WebsocketServer(0, loglevel=logging.DEBUG) - server_thread = Thread(target=s.run_forever) - server_thread.daemon = True - server_thread.start() - yield s - s.server_close() - - -@pytest.fixture -def session(server): - ws = create_connection("ws://{}:{}".format(*server.server_address)) - yield ws, server - ws.close() +from utils import session, server def test_text_message_of_length_1(session): diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..0bd9933 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,26 @@ +import logging +from threading import Thread + +import pytest +from websocket import create_connection # websocket-client + +import _bootstrap_ +from websocket_server import WebsocketServer + + +@pytest.fixture(scope='function') +def server(): + """ Returns the response of a server after""" + s = WebsocketServer(0, loglevel=logging.DEBUG) + server_thread = Thread(target=s.run_forever) + server_thread.daemon = True + server_thread.start() + yield s + s.server_close() + + +@pytest.fixture +def session(server): + ws = create_connection("ws://{}:{}".format(*server.server_address)) + yield ws, server + ws.close() diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index f25a35a..7699e61 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -310,15 +310,17 @@ def handshake(self): self.valid_client = True self.server._new_client_(self) - def make_handshake_response(self, key): + @classmethod + def make_handshake_response(cls, key): return \ 'HTTP/1.1 101 Switching Protocols\r\n'\ 'Upgrade: websocket\r\n' \ 'Connection: Upgrade\r\n' \ 'Sec-WebSocket-Accept: %s\r\n' \ - '\r\n' % self.calculate_response_key(key) + '\r\n' % cls.calculate_response_key(key) - def calculate_response_key(self, key): + @classmethod + def calculate_response_key(cls, key): GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' hash = sha1(key.encode() + GUID.encode()) response_key = b64encode(hash.digest()).strip() From 9bd9c4051a610ec5e47dbe531fbafbbfe9d5ecdc Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 22:47:25 +0000 Subject: [PATCH 039/141] Check for ConnectionResetError when reading bytes from client --- websocket_server/websocket_server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 7699e61..a79e6f7 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -190,6 +190,10 @@ def read_bytes(self, num): def read_next_message(self): try: b1, b2 = self.read_bytes(2) + except ConnectionResetError: + logger.info("Client closed connection.") + self.keep_alive = 0 + return except ValueError as e: b1, b2 = 0, 0 @@ -198,10 +202,6 @@ def read_next_message(self): masked = b2 & MASKED payload_length = b2 & PAYLOAD_LEN - if not b1: - logger.info("Client closed connection.") - self.keep_alive = 0 - return if opcode == OPCODE_CLOSE_CONN: logger.info("Client asked to close connection.") self.keep_alive = 0 From c3f0b00fbd29e478b814b4ea32b0b7d479e89841 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 22:48:27 +0000 Subject: [PATCH 040/141] Read HTTP headers properly no matter of size (i.e. larger than buffer previously) --- websocket_server/websocket_server.py | 31 +++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index a79e6f7..7bb5db4 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -292,19 +292,36 @@ def send_text(self, message, opcode=OPCODE_TEXT): self.request.send(header + payload) + def read_http_headers(self): + headers = {} + # first line should be HTTP GET + http_get = self.rfile.readline().decode().strip() + assert http_get.upper().startswith('GET') + # remaining should be headers + while True: + header = self.rfile.readline().decode().strip() + if not header: + break + head, value = header.split(':', 1) + headers[head.lower().strip()] = value.strip() + return headers + def handshake(self): - message = self.request.recv(1024).decode().strip() - upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) - if not upgrade: + headers = self.read_http_headers() + + try: + assert headers['upgrade'].lower() == 'websocket' + except AssertionError: self.keep_alive = False return - key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) - if key: - key = key.group(1) - else: + + try: + key = headers['sec-websocket-key'] + except KeyError: logger.warning("Client tried to connect but was missing a key") self.keep_alive = False return + response = self.make_handshake_response(key) self.handshake_done = self.request.send(response.encode()) self.valid_client = True From 80479f44b119e96fbf358cd5dcae7a109bffb33a Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 22:51:13 +0000 Subject: [PATCH 041/141] Remove unused modules --- websocket_server/websocket_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 7bb5db4..044f8d3 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -1,7 +1,6 @@ # Author: Johan Hanssen Seferidis # License: MIT -import re import sys import struct from base64 import b64encode From b5f3f6ef5765a62fc76cd505b9f6b97b9c794caf Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:07:33 +0000 Subject: [PATCH 042/141] Add CircleCI config --- .circleci/config.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..521a6a8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,37 @@ +# Python CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-python/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` + - image: circleci/python:3.6.1 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/repo + + steps: + - checkout + + - run: + name: install dependencies + command: | + virtualenv .env + source .env + pip install tox + + - run: + name: run tests + command: | + tox + + - store_artifacts: + path: test-reports + destination: test-reports From 97983d1652588c364b13b8761b90dad50cde1e61 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:11:40 +0000 Subject: [PATCH 043/141] Use generic Python3 container --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 521a6a8..9acff01 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: docker: # specify the version you desire here # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: circleci/python:3.6.1 + - image: circleci/python:3 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images From 790728cece4317c8c7e4a6c8dad55838820851d8 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:14:36 +0000 Subject: [PATCH 044/141] Use virtualenv on CircleCI --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9acff01..9990152 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,8 +23,8 @@ jobs: - run: name: install dependencies command: | - virtualenv .env - source .env + python3 -m venv .env + . venv/bin/activate pip install tox - run: From 034b1772dc156a0c1311499f494401488a3a3d4f Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:18:31 +0000 Subject: [PATCH 045/141] Fix CircleCI config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9990152..592142e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ jobs: - run: name: install dependencies command: | - python3 -m venv .env + python3 -m venv venv . venv/bin/activate pip install tox From 07287ed5ee680992d0dced99718765745f148605 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:21:24 +0000 Subject: [PATCH 046/141] Fix CircleCI config --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 592142e..24dbfc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,6 +30,7 @@ jobs: - run: name: run tests command: | + . venv/bin/activate tox - store_artifacts: From 33831487d962ab80930bab4b51068f79e25d7dd1 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:36:16 +0000 Subject: [PATCH 047/141] Update README and add badge --- README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cd01640..0ed6676 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ Websocket Server ======================= +[![CircleCI](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master.svg?style=svg)](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master) + A minimal Websockets Server in Python with no external dependencies. - * Works with Python2 and Python3 + * Python2 and Python3 support * Clean simple API * Multiple clients * No dependencies @@ -13,15 +15,6 @@ like SSL etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. -Usage -======================= -You can get a feel of how to use the websocket server by running - - python server.py - -Then just open `client.html` in your browser and you should be able to send and receive messages. - - Installation ======================= @@ -34,6 +27,15 @@ You can use the project in three ways. For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api). +Usage +======================= +You can get a feel of how to use the websocket server by running + + python server.py + +Then just open `client.html` in your browser and you should be able to send and receive messages. + + Testing ======= From b38d1dfca1ee55eb9f7a6f37d228f834de59aa08 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Mon, 5 Mar 2018 23:38:05 +0000 Subject: [PATCH 048/141] Remove garbage --- tests/message_lengths.py | 57 ---------------------------------------- 1 file changed, 57 deletions(-) delete mode 100644 tests/message_lengths.py diff --git a/tests/message_lengths.py b/tests/message_lengths.py deleted file mode 100644 index 8ed590a..0000000 --- a/tests/message_lengths.py +++ /dev/null @@ -1,57 +0,0 @@ -import _bootstrap_ -from websocket_server import WebsocketServer -from time import sleep -from testsuite.messages import * - -''' -This creates just a server that will send a different message to every new connection: - - 1. A message of length less than 126 - 2. A message of length 126 - 3. A message of length 127 - 4. A message of length bigger than 127 - 5. A message above 1024 - 6. A message above 65K - 7. An enormous message (well beyond 65K) - - -Reconnect to get the next message -''' - - -counter = 0 - -# Called for every client connecting (after handshake) -def new_client(client, server): - print("New client connected and was given id %d" % client['id']) - global counter - if counter == 0: - print("Sending message 1 of length %d" % len(msg_125B)) - server.send_message(client, msg_125B) - elif counter == 1: - print("Sending message 2 of length %d" % len(msg_126B)) - server.send_message(client, msg_126B) - elif counter == 2: - print("Sending message 3 of length %d" % len(msg_127B)) - server.send_message(client, msg_127B) - elif counter == 3: - print("Sending message 4 of length %d" % len(msg_208B)) - server.send_message(client, msg_208B) - elif counter == 4: - print("Sending message 5 of length %d" % len(msg_1251B)) - server.send_message(client, msg_1251B) - elif counter == 5: - print("Sending message 6 of length %d" % len(msg_68KB)) - server.send_message(client, msg_68KB) - elif counter == 6: - print("Sending message 7 of length %d" % len(msg_1500KB)) - server.send_message(client, msg_1500KB) - else: - print("No errors") - counter += 1 - - -PORT=9001 -server = WebsocketServer(PORT) -server.set_fn_new_client(new_client) -server.run_forever() From 8bec0db26995fc2eabe2b90b969d29b07fa300d7 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Tue, 6 Mar 2018 17:59:47 +0000 Subject: [PATCH 049/141] Test simplifying CircleCI config --- .circleci/config.yml | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 24dbfc1..d973c3e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,35 +4,15 @@ # version: 2 jobs: - build: + + test: docker: - # specify the version you desire here - # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - image: circleci/python:3 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/postgres:9.4 - - working_directory: ~/repo - - steps: - - checkout - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip install tox - - - run: - name: run tests - command: | - . venv/bin/activate - tox - - - store_artifacts: - path: test-reports - destination: test-reports + - run: + name: run tests + command: | + pip install tox + tox + - store_artifacts: + path: test-reports + destination: test-reports From 7d52f5755451048d27b19406b7a165de8efceea5 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Tue, 6 Mar 2018 18:03:56 +0000 Subject: [PATCH 050/141] Test simplifying CircleCI config --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d973c3e..26a776d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,8 +4,7 @@ # version: 2 jobs: - - test: + build: docker: - image: circleci/python:3 - run: From dabe0abd5bc23f24e1e43284f71adf6fe00912b3 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Tue, 6 Mar 2018 18:07:10 +0000 Subject: [PATCH 051/141] Test simplifying CircleCI config --- .circleci/config.yml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 26a776d..f4422f9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,11 +7,16 @@ jobs: build: docker: - image: circleci/python:3 - - run: - name: run tests - command: | - pip install tox - tox - - store_artifacts: - path: test-reports - destination: test-reports + steps: + - checkout + - run: + name: install dependencies + command: | + pip install tox + - run: + name: run tests + command: | + tox + - store_artifacts: + path: test-reports + destination: test-reports From 626dfab2121c91a6af3b426b871363986fd5b386 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Tue, 6 Mar 2018 18:09:06 +0000 Subject: [PATCH 052/141] Test simplifying CircleCI config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f4422f9..4eddce6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: - run: name: install dependencies command: | - pip install tox + sudo pip install tox - run: name: run tests command: | From 433a8263fcaefb47ce4984f2600b2eeba024054a Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Wed, 7 Mar 2018 11:10:06 +0000 Subject: [PATCH 053/141] Add test for unicode characters --- tests/{test_message_lengths.py => test_text_messages.py} | 8 ++++++++ 1 file changed, 8 insertions(+) rename tests/{test_message_lengths.py => test_text_messages.py} (92%) diff --git a/tests/test_message_lengths.py b/tests/test_text_messages.py similarity index 92% rename from tests/test_message_lengths.py rename to tests/test_text_messages.py index 6fe3539..d20302c 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_text_messages.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from utils import session, server @@ -68,3 +69,10 @@ def test_text_message_of_length_1500KB(session): assert len(msg) == 1500000 server.send_message_to_all(msg) assert client.recv() == msg + + +def test_text_message_with_unicode_characters(session): + client, server = session + msg = '$äüö^' + server.send_message_to_all(msg) + assert client.recv() == msg From bc4878d27b7679557d65b1e124caf126261d7a92 Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Thu, 8 Mar 2018 13:14:42 +0000 Subject: [PATCH 054/141] Add unfinished stress test --- tests/test_text_messages.py | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_text_messages.py b/tests/test_text_messages.py index d20302c..6090f4c 100644 --- a/tests/test_text_messages.py +++ b/tests/test_text_messages.py @@ -76,3 +76,53 @@ def test_text_message_with_unicode_characters(session): msg = '$äüö^' server.send_message_to_all(msg) assert client.recv() == msg + + +def test_text_message_stress_bursts(session): + """ Scenario: server sends multiple different message to the same client + at once """ + from threading import Thread + NUM_THREADS = 100 + MESSAGE_LEN = 1000 + client, server = session + messages_received = [] + + # Threads receing + threads_receiving = [] + for i in range(NUM_THREADS): + th = Thread( + target=lambda fn: messages_received.append(fn()), + args=(client.recv,) + ) + th.daemon = True + threads_receiving.append(th) + + # Threads sending different characters each of them + threads_sending = [] + for i in range(NUM_THREADS): + message = chr(i)*MESSAGE_LEN + th = Thread( + target=server.send_message_to_all, + args=(message,) + ) + th.daemon = True + threads_sending.append(th) + + # Run scenario + for th in threads_receiving: + th.start() + for th in threads_sending: + th.start() + + # Wait for all threads to finish + print('WAITING FOR THREADS TO FINISH') + for th in threads_receiving: + th.join() + for th in threads_sending: + th.join() + + for message in messages_received: + first_char = message[0] + assert message.count(first_char) == len(message) + + print() From 672d52baafb13377f60b1442db3ad87ce8583843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Andr=C3=A9s=20=C3=81lvarez=20Restrepo?= Date: Thu, 8 Mar 2018 15:39:47 -0800 Subject: [PATCH 055/141] python 2.7 crashes handling error ConnectionResetError Hi! I was running the server in python 2.7 but it crashed when it tried to handle the *ConnectionResetError*, because I think it is not defined in python 2.7. I'm proposing this fix, although I dont know if there's a better way to do it. --- websocket_server/websocket_server.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 777046d..bd403d6 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -6,6 +6,8 @@ from base64 import b64encode from hashlib import sha1 import logging +from socket import error as SocketError +import errno if sys.version_info[0] < 3: from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler @@ -189,10 +191,13 @@ def read_bytes(self, num): def read_next_message(self): try: b1, b2 = self.read_bytes(2) - except ConnectionResetError: - logger.info("Client closed connection.") - self.keep_alive = 0 - return + except SocketError as e: + if e.errno == errno.ECONNRESET: + logger.info("Client closed connection.") + print("Error: {}".format(e)) + self.keep_alive = 0 + return + b1, b2 = 0, 0 except ValueError as e: b1, b2 = 0, 0 From 99d36b0cc917492b2298934179f5f30035c7d9ce Mon Sep 17 00:00:00 2001 From: "Manos S.H" Date: Fri, 9 Mar 2018 17:05:05 +0000 Subject: [PATCH 056/141] Small syntax error fix --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 777046d..8cf1cc0 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -222,7 +222,7 @@ def read_next_message(self): elif opcode == OPCODE_PONG: opcode_handler = self.server._pong_received_ else: - logger.warn("Unknown opcode %#x." + opcode) + logger.warn("Unknown opcode %#x." % opcode) self.keep_alive = 0 return From a1bdde58615d07b4d3bbcdfaf91e997057afc970 Mon Sep 17 00:00:00 2001 From: Manos Date: Tue, 13 Mar 2018 13:29:16 +0000 Subject: [PATCH 057/141] Add comments for future refactoring --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index bd403d6..73f91cc 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -191,7 +191,7 @@ def read_bytes(self, num): def read_next_message(self): try: b1, b2 = self.read_bytes(2) - except SocketError as e: + except SocketError as e: # to be replaced with ConnectionResetError for py3 if e.errno == errno.ECONNRESET: logger.info("Client closed connection.") print("Error: {}".format(e)) From 660c5e97597bdb500b5a319879fe8aa5bf76d605 Mon Sep 17 00:00:00 2001 From: Alain Hernandez Date: Tue, 7 May 2019 18:31:31 -0700 Subject: [PATCH 058/141] removed print statement --- websocket_server/websocket_server.py | 1 - 1 file changed, 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 9d1af5c..96c658b 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -194,7 +194,6 @@ def read_next_message(self): except SocketError as e: # to be replaced with ConnectionResetError for py3 if e.errno == errno.ECONNRESET: logger.info("Client closed connection.") - print("Error: {}".format(e)) self.keep_alive = 0 return b1, b2 = 0, 0 From b4a730465a8330f525116eb4faf21d21877a0986 Mon Sep 17 00:00:00 2001 From: Omar Elamri Date: Wed, 24 Jul 2019 13:28:11 -0500 Subject: [PATCH 059/141] Add ssl support --- websocket_server/websocket_server.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 96c658b..37fc79a 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -3,6 +3,7 @@ import sys import struct +import ssl from base64 import b64encode from hashlib import sha1 import logging @@ -112,16 +113,22 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): } """ + key = None + cert = None + allow_reuse_address = True daemon_threads = True # comment to keep threads alive until finished clients = [] id_counter = 0 - def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): + def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, cert=None): logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) self.port = self.socket.getsockname()[1] + + self.key = key + self.cert = cert def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) @@ -165,6 +172,11 @@ class WebSocketHandler(StreamRequestHandler): def __init__(self, socket, addr, server): self.server = server + if server.key and server.cert: + try: + socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) + except: # Not sure which exception it throws if the key/cert isn't found + logger.warn("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) StreamRequestHandler.__init__(self, socket, addr, server) def setup(self): From 3906fe8016328574cda89d1f3507f7ffc011707f Mon Sep 17 00:00:00 2001 From: Omar Elamri Date: Wed, 24 Jul 2019 13:33:25 -0500 Subject: [PATCH 060/141] Fix indentation --- websocket_server/websocket_server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 37fc79a..8fd167a 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -113,8 +113,8 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): } """ - key = None - cert = None + key = None + cert = None allow_reuse_address = True daemon_threads = True # comment to keep threads alive until finished @@ -127,8 +127,8 @@ def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, c TCPServer.__init__(self, (host, port), WebSocketHandler) self.port = self.socket.getsockname()[1] - self.key = key - self.cert = cert + self.key = key + self.cert = cert def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) @@ -172,11 +172,11 @@ class WebSocketHandler(StreamRequestHandler): def __init__(self, socket, addr, server): self.server = server - if server.key and server.cert: - try: - socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) - except: # Not sure which exception it throws if the key/cert isn't found - logger.warn("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) + if server.key and server.cert: + try: + socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) + except: # Not sure which exception it throws if the key/cert isn't found + logger.warn("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) StreamRequestHandler.__init__(self, socket, addr, server) def setup(self): From 260782f56ec882c0f757d54aa4f4009564538cc1 Mon Sep 17 00:00:00 2001 From: Omar Elamri Date: Wed, 24 Jul 2019 13:37:35 -0500 Subject: [PATCH 061/141] Update README.md with SSL documentation changes. --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ed6676..169f2fe 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ The WebsocketServer can be initialized with the below parameters. *`loglevel`* - logging level to print. By default WARNING is used. You can use `logging.DEBUG` or `logging.INFO` for more verbose output. +*`key`* - If using SSL, this is the path to the key. + +*`cert`* - If using SSL, this is the path to the certificate. + ### Properties @@ -101,7 +105,19 @@ def new_client(client, server): server = WebsocketServer(13254, host='127.0.0.1', loglevel=logging.INFO) server.set_fn_new_client(new_client) server.run_forever() -```` +```` +Example (SSL): +````py +import logging +from websocket_server import WebsocketServer + +def new_client(client, server): + server.send_message_to_all("Hey all, a new client has joined us") + +server = WebsocketServer(13254, host='127.0.0.1', loglevel=logging.INFO, key="key.pem", cert="cert.pem") +server.set_fn_new_client(new_client) +server.run_forever() +```` ## Client From d2b11f476773869dcc69efec5307b8a6091ce952 Mon Sep 17 00:00:00 2001 From: Omar Elamri Date: Wed, 24 Jul 2019 13:39:04 -0500 Subject: [PATCH 062/141] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 169f2fe..cfb20a6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A minimal Websockets Server in Python with no external dependencies. * No dependencies Notice that this implementation does not support the more advanced features -like SSL etc. The project is focused mainly on making it easy to run a +like multithreading etc. The project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. From a3dd5aba359bec138b814d0f8b1722512dfd9074 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 15:07:56 +0100 Subject: [PATCH 063/141] Drop official support for python2 --- README.md | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0ed6676..167f79c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Websocket Server A minimal Websockets Server in Python with no external dependencies. - * Python2 and Python3 support + * Python3.5+ * Clean simple API * Multiple clients * No dependencies diff --git a/tox.ini b/tox.ini index b3025e9..588f7ab 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py3 +envlist = py35,py36,py39 [testenv] deps=pytest websocket-client From a59ac0c73c098996c1c9461ce661963f1dec93ae Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 15:21:38 +0100 Subject: [PATCH 064/141] Add tests for message lengths --- tests/test_message_lengths.py | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 tests/test_message_lengths.py diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py new file mode 100644 index 0000000..14fec70 --- /dev/null +++ b/tests/test_message_lengths.py @@ -0,0 +1,96 @@ +from time import sleep +import logging +from threading import Thread + +import pytest +from websocket import create_connection # websocket-client + +import _bootstrap_ +from websocket_server import WebsocketServer + + +@pytest.fixture(scope='function') +def server(): + """ Returns the response of a server after""" + s = WebsocketServer(0, loglevel=logging.DEBUG) + server_thread = Thread(target=s.run_forever) + server_thread.daemon = True + server_thread.start() + yield s + s.server_close() + + +@pytest.fixture +def session(server): + ws = create_connection("ws://{}:{}".format(*server.server_address)) + yield ws, server + ws.close() + + +def test_text_message_of_length_1(session): + client, server = session + server.send_message_to_all('$') + assert client.recv() == '$' + + +def test_text_message_of_length_125B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqr125' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_126B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrs126' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_127B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrst127' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_208B(session): + client, server = session + msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw208' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_1251B(session): + client, server = session + msg = ('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ + 'abcdefghijklmnopqr125'*10)+'1' + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_68KB(session): + client, server = session + msg = '$'+('a'*67993)+'68000'+'^' + assert len(msg) == 68000 + server.send_message_to_all(msg) + assert client.recv() == msg + + +def test_text_message_of_length_1500KB(session): + """ An enormous message (well beyond 65K) """ + client, server = session + msg = '$'+('a'*1499991)+'1500000'+'^' + assert len(msg) == 1500000 + server.send_message_to_all(msg) + assert client.recv() == msg From 0a85098492377eb077f7a35fdbc19ac2663d262f Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 15:40:47 +0100 Subject: [PATCH 065/141] Replace py39 with py37 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 588f7ab..e94ca52 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,6 @@ [tox] -envlist = py35,py36,py39 +envlist = py35,py36,py37 + [testenv] deps=pytest websocket-client From f2776dc3e8351b7ae5cb812532eeeb900c212b9e Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 15:46:04 +0100 Subject: [PATCH 066/141] Obliterate python2 specific code --- websocket_server/websocket_server.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 96c658b..0fd4b4c 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -9,10 +9,7 @@ from socket import error as SocketError import errno -if sys.version_info[0] < 3: - from SocketServer import ThreadingMixIn, TCPServer, StreamRequestHandler -else: - from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler +from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler logger = logging.getLogger(__name__) logging.basicConfig() @@ -181,12 +178,7 @@ def handle(self): self.read_next_message() def read_bytes(self, num): - # python3 gives ordinal of byte directly - bytes = self.rfile.read(num) - if sys.version_info[0] < 3: - return map(ord, bytes) - else: - return bytes + return self.rfile.read(num) def read_next_message(self): try: @@ -260,12 +252,8 @@ def send_text(self, message, opcode=OPCODE_TEXT): if not message: logger.warning("Can\'t send message, message is not valid UTF-8") return False - elif sys.version_info < (3,0) and (isinstance(message, str) or isinstance(message, unicode)): - pass - elif isinstance(message, str): - pass - else: - logger.warning('Can\'t send message, message has to be a string or bytes. Given type is %s' % type(message)) + elif not isinstance(message, str): + logger.warning('Can\'t send message, message has to be a string or bytes. Got %s' % type(message)) return False header = bytearray() From 33082d80f37b28d325e61e691c2f76bb9d52478e Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 15:50:12 +0100 Subject: [PATCH 067/141] Make clients and client counter instance specific --- websocket_server/websocket_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 0fd4b4c..5ce104f 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -112,14 +112,14 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): allow_reuse_address = True daemon_threads = True # comment to keep threads alive until finished - clients = [] - id_counter = 0 - def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING): logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) self.port = self.socket.getsockname()[1] + self.clients = [] + self.id_counter = 0 + def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) From f82a27af5147e22cc42e28fdf88b3e58378c02de Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:23:51 +0100 Subject: [PATCH 068/141] Add requirements --- requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3988e71 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# Dev/test/deploy +tox>=3.24.0 +IPython +pytest +websocket-client +twine From fb1224b072b8429c2e367a357b05be38140f4094 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:24:49 +0100 Subject: [PATCH 069/141] Update circleCI to deploy new releases based on tag --- .circleci/config.yml | 58 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4eddce6..1c8b423 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,10 @@ -# Python CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-python/ for more details -# -version: 2 +version: 2.1 + +orbs: + python: circleci/python@0.2.1 + jobs: - build: + test: docker: - image: circleci/python:3 steps: @@ -20,3 +20,49 @@ jobs: - store_artifacts: path: test-reports destination: test-reports + deploy: + executor: python/default + steps: + - checkout + - python/load-cache + - run: + name: verify git tag vs. version + command: | + python setup.py verify + - run: + name: create packages + command: | + python setup.py sdist + python setup.py bdist_wheel + - run: + name: setup pypi credentials + command: | + echo -e "[pypi]" >> ~/.pypirc + echo -e "username = $PYPI_USERNAME" >> ~/.pypirc + echo -e "password = $PYPI_PASSWORD" >> ~/.pypirc + - run: + name: upload to cheeseshop if a tag found + command: | + tag=`git tag --points-at HEAD` + if [ $tag ]; then + python -m twine upload dist/* + fi + +workflows: + test: + jobs: + - test: + filters: + branches: + only: development + new_release: + jobs: + - test: + filters: + branches: + only: master + tags: + only: /v[0-9]+(\.[0-9]+)*/ + - deploy: + requires: + - test From 7b3e0a00f3dc1904a284181d6539c3ac2a313460 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:41:02 +0100 Subject: [PATCH 070/141] CircleCI now uses all tox python versions --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c8b423..8b8f689 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,9 @@ jobs: - run: name: install dependencies command: | + sudo apt-get install python3.5 + sudo apt-get install python3.6 + sudo apt-get install python3.7 sudo pip install tox - run: name: run tests From 0304730a8f7f39472d080040c6b9c3644cc5dc83 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:46:17 +0100 Subject: [PATCH 071/141] Test only python3.7 --- .circleci/config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8b8f689..75aeefa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,14 +12,11 @@ jobs: - run: name: install dependencies command: | - sudo apt-get install python3.5 - sudo apt-get install python3.6 - sudo apt-get install python3.7 sudo pip install tox - run: name: run tests command: | - tox + tox -e py37 - store_artifacts: path: test-reports destination: test-reports From f34ffc5ae314ece091a54f06f6e20b2ca6b9240c Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:48:16 +0100 Subject: [PATCH 072/141] Bump up version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b495e1b..99f530e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='websocket_server', - version='0.4', + version='0.5', packages=find_packages("."), url='https://github.com/Pithikos/python-websocket-server', license='MIT', From 3ef369f27656ba52dd7b48c75d14669ef79676d4 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:50:16 +0100 Subject: [PATCH 073/141] Add release doc --- docs/release.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docs/release.md diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..346309c --- /dev/null +++ b/docs/release.md @@ -0,0 +1,12 @@ +Release notes +------------- + +Releases are marked on master branch with tags. The upload to pypi is automated as long as a merge +from development comes with a tag. + +General flow + + 1. Update VERSION in setup.py from development branch and commit + 2. Merge development into master (`git merge --no-ff development`) + 3. Add corresponding version as a new tag (`git tag `) e.g. git tag v0.3.0 + 4. Push everything (`git push --tags && git push`) From cbc19527aa03cf4c8da5bfa1f428a964b191f2c6 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 16:57:39 +0100 Subject: [PATCH 074/141] Fix verification step --- setup.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 99f530e..4b5fcb8 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,46 @@ -from setuptools import setup, find_packages +import os +import sys +import re +import subprocess +import shlex + +try: + from setuptools import setup + from setuptools.command.install import install +except ImportError: + from distutils.core import setup + from distutils.command.install import install + + +VERSION = '0.5' + + +def get_tag_version(): + cmd = 'git tag --points-at HEAD' + versions = subprocess.check_output(shlex.split(cmd)).splitlines() + if not versions: + return None + if len(versions) != 1: + sys.exit(f"Trying to get tag via git: Expected excactly one tag, got {len(versions)}") + version = versions[0].decode() + if re.match('^v[0-9]', version): + version = version[1:] + return version + + +class VerifyVersionCommand(install): + """ Custom command to verify that the git tag matches our version """ + description = 'verify that the git tag matches our version' + + def run(self): + tag_version = get_tag_version() + if tag_version and tag_version != VERSION: + sys.exit(f"Git tag: {tag} does not match the version of this app: {VERSION}") + setup( name='websocket_server', - version='0.5', + version=VERSION, packages=find_packages("."), url='https://github.com/Pithikos/python-websocket-server', license='MIT', @@ -12,4 +50,7 @@ ], description='A simple fully working websocket-server in Python with no external dependencies', platforms='any', + cmdclass={ + 'verify': VerifyVersionCommand, + }, ) From 02c4abbc11ad087425b07e2c69db2772fa2bb084 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 17:01:31 +0100 Subject: [PATCH 075/141] Fix imports --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 4b5fcb8..250aa7c 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,10 @@ import shlex try: - from setuptools import setup + from setuptools import setup, find_packages from setuptools.command.install import install except ImportError: - from distutils.core import setup + from distutils.core import setup, find_packages from distutils.command.install import install From 5167c81342cf7af7c74108065b8a84735b8e127e Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 17:05:38 +0100 Subject: [PATCH 076/141] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 250aa7c..4782866 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.5' +VERSION = '0.5.1' def get_tag_version(): From 2927b431ed5fa19b6a1f989ead4f373acb8175c2 Mon Sep 17 00:00:00 2001 From: Mano Date: Wed, 14 Jul 2021 17:53:27 +0100 Subject: [PATCH 077/141] Add twine to workflow --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 75aeefa..65bbd8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,6 +25,10 @@ jobs: steps: - checkout - python/load-cache + - run: + name: install twine + command: | + sudo pip install twine - run: name: verify git tag vs. version command: | From e0ffd1ecd5cd7d2a4e398fb128904eced9dd740e Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 09:08:36 +0100 Subject: [PATCH 078/141] Cleanup readme --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 167f79c..05d2f2e 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,9 @@ websocket server for prototyping, testing or for making a GUI for your applicati Installation ======================= -You can use the project in three ways. +Install with pip - 1. Copy/paste the *websocket_server.py* file in your project and use it directly - 2. `pip install git+https://github.com/Pithikos/python-websocket-server` (latest code) - 3. `pip install websocket-server` (might not be up-to-date) + pip install websocket-server For coding details have a look at the [*server.py*](https://github.com/Pithikos/python-websocket-server/blob/master/server.py) example and the [API](https://github.com/Pithikos/python-websocket-server#api). From 1cf4f71fcc355138649b8a1fb422d27e4d4c90b4 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 09:16:34 +0100 Subject: [PATCH 079/141] Remove comments --- websocket_server/websocket_server.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 5ce104f..7b9b1bf 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -45,8 +45,6 @@ OPCODE_PONG = 0xA -# -------------------------------- API --------------------------------- - class API(): def run_forever(self): @@ -85,8 +83,6 @@ def send_message_to_all(self, msg): self._multicast_(msg) -# ------------------------- Implementation ----------------------------- - class WebsocketServer(ThreadingMixIn, TCPServer, API): """ A websocket server waiting for clients to connect. From 2160548815592ca7484f6783e9dffee8aed0d5a7 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 09:18:04 +0100 Subject: [PATCH 080/141] Cleanup functions --- websocket_server/websocket_server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 7b9b1bf..74a7f99 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -77,10 +77,10 @@ def set_fn_message_received(self, fn): self.message_received = fn def send_message(self, client, msg): - self._unicast_(client, msg) + self._unicast(client, msg) def send_message_to_all(self, msg): - self._multicast_(msg) + self._multicast(msg) class WebsocketServer(ThreadingMixIn, TCPServer, API): @@ -141,12 +141,12 @@ def _client_left_(self, handler): if client in self.clients: self.clients.remove(client) - def _unicast_(self, to_client, msg): + def _unicast(self, to_client, msg): to_client['handler'].send_message(msg) - def _multicast_(self, msg): + def _multicast(self, msg): for client in self.clients: - self._unicast_(client, msg) + self._unicast(client, msg) def handler_to_client(self, handler): for client in self.clients: From 25ef5c2059e949f37a14f6c58e330fdac2da3556 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 10:03:52 +0100 Subject: [PATCH 081/141] Add version badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05d2f2e..7c90c0c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Websocket Server ======================= -[![CircleCI](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master.svg?style=svg)](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master) +[![CircleCI](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master.svg?style=svg)](https://circleci.com/gh/Pithikos/python-websocket-server/tree/master) [![PyPI version](https://badge.fury.io/py/websocket-server.svg)](https://badge.fury.io/py/websocket-server) A minimal Websockets Server in Python with no external dependencies. From a7505581fe41776e28302fd6feca326f9beaf218 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 10:48:13 +0100 Subject: [PATCH 082/141] Cleanup python2 ruins --- tests/test_text_messages.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_text_messages.py b/tests/test_text_messages.py index 6090f4c..28dd4e6 100644 --- a/tests/test_text_messages.py +++ b/tests/test_text_messages.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from utils import session, server From 72472be48454cf159221d84fc323b5d6acb4cf06 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 10:48:52 +0100 Subject: [PATCH 083/141] Add test_server.py --- tests/test_server.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/test_server.py diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..767785d --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,17 @@ +from utils import session, server + +import pytest + + +def test_client_closes_gracefully(session): + client, server = session + assert client.connected + assert server.clients + old_client_handler = server.clients[0]["handler"] + client.close() + assert not client.connected + + # Ensure server closed connection + assert not server.clients + with pytest.raises(BrokenPipeError): + old_client_handler.connection.send(b"test") From 7b5b13eeaa84b46d71dd9f6d49d4cae91bf6e2b3 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 11:04:20 +0100 Subject: [PATCH 084/141] Minor cleanup --- websocket_server/websocket_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index a8bd5e1..24d9250 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -145,8 +145,8 @@ def _client_left_(self, handler): if client in self.clients: self.clients.remove(client) - def _unicast(self, to_client, msg): - to_client['handler'].send_message(msg) + def _unicast(self, receiver_client, msg): + receiver_client['handler'].send_message(msg) def _multicast(self, msg): for client in self.clients: From c9ad220cc8af8d64849d235765a9ecd83597043f Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 15 Jul 2021 11:27:32 +0100 Subject: [PATCH 085/141] Add releases --- docs/release.md | 7 ++++--- releases.txt | 6 ++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 releases.txt diff --git a/docs/release.md b/docs/release.md index 346309c..8c0d723 100644 --- a/docs/release.md +++ b/docs/release.md @@ -7,6 +7,7 @@ from development comes with a tag. General flow 1. Update VERSION in setup.py from development branch and commit - 2. Merge development into master (`git merge --no-ff development`) - 3. Add corresponding version as a new tag (`git tag `) e.g. git tag v0.3.0 - 4. Push everything (`git push --tags && git push`) + 2. Update releases.txt + 3. Merge development into master (`git merge --no-ff development`) + 4. Add corresponding version as a new tag (`git tag `) e.g. git tag v0.3.0 + 5. Push everything (`git push --tags && git push`) diff --git a/releases.txt b/releases.txt new file mode 100644 index 0000000..633ac84 --- /dev/null +++ b/releases.txt @@ -0,0 +1,6 @@ +0.4 +- Python 2 and 3 support + +0.5.1 +- SSL support +- Drop Python 2 support From 3dd0e199e80073981ec87f4265e291416b55d7f9 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 20 Sep 2021 15:43:16 +0100 Subject: [PATCH 086/141] Clear whitespace --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 24d9250..fdb7d68 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -113,7 +113,7 @@ def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, c logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) self.port = self.socket.getsockname()[1] - + self.key = key self.cert = cert From 2901f6a1d2019dbc5930ad54eecc3401ea3f6db6 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 20 Sep 2021 15:50:27 +0100 Subject: [PATCH 087/141] Require at least wsclient that fixes some race condition --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3988e71..053c4e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ tox>=3.24.0 IPython pytest -websocket-client +websocket-client>=1.1.1 twine From 1a1a188d6a2047a3faef102a23a19640ec509fb5 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 20 Sep 2021 15:56:33 +0100 Subject: [PATCH 088/141] Update doc --- docs/{release.md => release-workflow.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{release.md => release-workflow.md} (100%) diff --git a/docs/release.md b/docs/release-workflow.md similarity index 100% rename from docs/release.md rename to docs/release-workflow.md From bbeaba9d3edc91dcdec4593ab0382e35df13b9e9 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 20 Sep 2021 16:03:07 +0100 Subject: [PATCH 089/141] Add comments to test --- tests/test_server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 767785d..8f7642f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,7 +11,9 @@ def test_client_closes_gracefully(session): client.close() assert not client.connected - # Ensure server closed connection + # Ensure server closed connection. + # We test this by having the server trying to send + # data to the client assert not server.clients with pytest.raises(BrokenPipeError): old_client_handler.connection.send(b"test") From bf0109434899a4450552349a4c2c9615f9f46ef6 Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 23 Sep 2021 16:10:13 +0100 Subject: [PATCH 090/141] Add client_session fixture --- tests/utils.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index 0bd9933..bc1a17b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,42 @@ import logging +from time import sleep from threading import Thread import pytest -from websocket import create_connection # websocket-client +import websocket # websocket-client import _bootstrap_ from websocket_server import WebsocketServer +class TestClient(): + def __init__(self, port, threaded=True): + websocket.enableTrace(True) + self.ws = websocket.WebSocketApp(f"ws://localhost:{port}/", + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close) + if threaded: + self.thread = Thread(target=self.ws.run_forever) + self.thread.daemon = True + self.thread.start() + else: + self.ws.run_forever() + + def on_message(self, ws, message): + print(f"Client: on_message: {message}") + + def on_error(self, ws, error): + print(f"Client: on_error: {error}") + + def on_close(self, ws, close_status_code, close_msg): + print("Client: on_close") + + def on_open(self, ws): + print("Client: on_open") + + @pytest.fixture(scope='function') def server(): """ Returns the response of a server after""" @@ -21,6 +50,21 @@ def server(): @pytest.fixture def session(server): - ws = create_connection("ws://{}:{}".format(*server.server_address)) - yield ws, server - ws.close() + """ + Gives a simple connection to a server + """ + conn = websocket.create_connection("ws://{}:{}".format(*server.server_address)) + yield conn, server + client.ws.close() + + +@pytest.fixture +def client_session(server): + """ + Gives a TestClient instance connected to a server + """ + client = TestClient(port=server.port) + sleep(1) + assert client.ws.sock and client.ws.sock.connected + yield client, server + client.ws.close() From 1b1f6da35fa07c1e117e78ec4b8d650101210c9a Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 23 Sep 2021 16:19:37 +0100 Subject: [PATCH 091/141] Make TestClient to accumulate messages received --- tests/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/utils.py b/tests/utils.py index bc1a17b..b874350 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,6 +11,7 @@ class TestClient(): def __init__(self, port, threaded=True): + self.received_messages = [] websocket.enableTrace(True) self.ws = websocket.WebSocketApp(f"ws://localhost:{port}/", on_open=self.on_open, @@ -25,6 +26,7 @@ def __init__(self, port, threaded=True): self.ws.run_forever() def on_message(self, ws, message): + self.received_messages.append(message) print(f"Client: on_message: {message}") def on_error(self, ws, error): From 7b7367c35d9e3ff45ec4d2409c4257b684aafbfc Mon Sep 17 00:00:00 2001 From: Mano Date: Thu, 23 Sep 2021 16:26:23 +0100 Subject: [PATCH 092/141] Add send_close method --- tests/test_server.py | 20 +++++++++++++++++++- websocket_server/websocket_server.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 8f7642f..756f10e 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,8 +1,26 @@ -from utils import session, server +from utils import session, client_session, server +from time import sleep import pytest +def test_send_close(client_session): + "Ensure client stops receiving data once we send_close (socket is still open)" + client, server = client_session + assert client.received_messages == [] + + server.send_message_to_all("test1") + sleep(0.5) + assert client.received_messages == ["test1"] + + # After CLOSE, client should not be receiving any messages + server.clients[-1]["handler"].send_close() + sleep(0.5) + server.send_message_to_all("test2") + sleep(0.5) + assert client.received_messages == ["test1"] + + def test_client_closes_gracefully(session): client, server = session assert client.connected diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index fdb7d68..b83ed7f 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -45,6 +45,9 @@ OPCODE_PING = 0x9 OPCODE_PONG = 0xA +CLOSE_STATUS_NORMAL = 1000 +DEFAULT_CLOSE_REASON = bytes('', encoding='utf-8') + class API(): @@ -245,6 +248,18 @@ def send_message(self, message): def send_pong(self, message): self.send_text(message, OPCODE_PONG) + def send_close(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + """ + Send CLOSE to client + + Args: + status: Status as defined in https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1 + reason: Text with reason of closing the connection + """ + if status < CLOSE_STATUS_NORMAL or status > 1015: + raise Exception(f"CLOSE status must be between 1000 and 1015, got {status}") + self.request.send(struct.pack('!H', status) + reason, OPCODE_CLOSE_CONN) + def send_text(self, message, opcode=OPCODE_TEXT): """ Important: Fragmented(=continuation) messages are not supported since From de94e8f89eab4589909f2dbf66635db68d1bef2f Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 09:50:44 +0100 Subject: [PATCH 093/141] Fixup TestClient messages --- tests/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index b874350..e8add0e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,16 +27,16 @@ def __init__(self, port, threaded=True): def on_message(self, ws, message): self.received_messages.append(message) - print(f"Client: on_message: {message}") + print(f"TestClient: on_message: {message}") def on_error(self, ws, error): - print(f"Client: on_error: {error}") + print(f"TestClient: on_error: {error}") def on_close(self, ws, close_status_code, close_msg): - print("Client: on_close") + print(f"TestClient: on_close: {close_status_code} - {close_msg}") def on_open(self, ws): - print("Client: on_open") + print("TestClient: on_open") @pytest.fixture(scope='function') From 5173a7f1bab72dda2db5c88bdec330b94e537a39 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 10:47:50 +0100 Subject: [PATCH 094/141] Implement shutdown_gracefully --- tests/test_server.py | 18 +++++++++++++++++- websocket_server/websocket_server.py | 23 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 756f10e..2f09012 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -5,7 +5,9 @@ def test_send_close(client_session): - "Ensure client stops receiving data once we send_close (socket is still open)" + """ + Ensure client stops receiving data once we send_close (socket is still open) + """ client, server = client_session assert client.received_messages == [] @@ -21,6 +23,20 @@ def test_send_close(client_session): assert client.received_messages == ["test1"] +def test_shutdown_gracefully(client_session): + client, server = client_session + assert client.ws.sock and client.ws.sock.connected + assert server.socket.fileno() > 0 + + server.shutdown_gracefully() + sleep(0.5) + + # Ensure all parties disconnected + assert not client.ws.sock + assert server.socket.fileno() == -1 + assert not server.clients + + def test_client_closes_gracefully(session): client, server = session assert client.connected diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index b83ed7f..b0346b6 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -86,6 +86,18 @@ def send_message(self, client, msg): def send_message_to_all(self, msg): self._multicast(msg) + def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + """ + Close with a websocket handshake + + 1. Send CLOSE to all clients + 2. Close TCP + """ + self.keep_alive = False + for client in self.clients: + client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) + self.server_close() + class WebsocketServer(ThreadingMixIn, TCPServer, API): """ @@ -258,7 +270,16 @@ def send_close(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): """ if status < CLOSE_STATUS_NORMAL or status > 1015: raise Exception(f"CLOSE status must be between 1000 and 1015, got {status}") - self.request.send(struct.pack('!H', status) + reason, OPCODE_CLOSE_CONN) + + header = bytearray() + payload = struct.pack('!H', status) + reason + payload_length = len(payload) + assert payload_length <= 125, "We only support short closing reasons at the moment" + + # Send CLOSE with status & reason + header.append(FIN | OPCODE_CLOSE_CONN) + header.append(payload_length) + self.request.send(header + payload) def send_text(self, message, opcode=OPCODE_TEXT): """ From 1c8d07021b1f2f7ab6bd6d2e85fc6cd122f8a3c2 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 11:06:25 +0100 Subject: [PATCH 095/141] Fix fixture session --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index e8add0e..9655e52 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -57,7 +57,7 @@ def session(server): """ conn = websocket.create_connection("ws://{}:{}".format(*server.server_address)) yield conn, server - client.ws.close() + conn.close() @pytest.fixture From e59e69597b08baab195813b7fbcd4fd19d01a9c8 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 16:28:05 +0100 Subject: [PATCH 096/141] Add TestServer to make testing easier --- tests/utils.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 9655e52..58db6be 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -39,10 +39,20 @@ def on_open(self, ws): print("TestClient: on_open") +class TestServer(WebsocketServer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.received_messages = [] + self.set_fn_message_received(self.handle_received_message) + + def handle_received_message(self, client, server, message): + self.received_messages.append(message) + + @pytest.fixture(scope='function') def server(): """ Returns the response of a server after""" - s = WebsocketServer(0, loglevel=logging.DEBUG) + s = TestServer(0, loglevel=logging.DEBUG) server_thread = Thread(target=s.run_forever) server_thread.daemon = True server_thread.start() From 84c7b8783b2c2c688b302d731b65e587c8c1274c Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:04:29 +0100 Subject: [PATCH 097/141] TestClient now accumulates all events --- tests/utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/utils.py b/tests/utils.py index 58db6be..2230a51 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -12,6 +12,10 @@ class TestClient(): def __init__(self, port, threaded=True): self.received_messages = [] + self.closes = [] + self.opens = [] + self.errors = [] + websocket.enableTrace(True) self.ws = websocket.WebSocketApp(f"ws://localhost:{port}/", on_open=self.on_open, @@ -30,12 +34,15 @@ def on_message(self, ws, message): print(f"TestClient: on_message: {message}") def on_error(self, ws, error): + self.errors.append(error) print(f"TestClient: on_error: {error}") def on_close(self, ws, close_status_code, close_msg): + self.closes.append((close_status_code, close_msg)) print(f"TestClient: on_close: {close_status_code} - {close_msg}") def on_open(self, ws): + self.opens.append(ws) print("TestClient: on_open") From b59f0ab149cae39bc597b2fa3d0fb4259a1abca5 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:29:32 +0100 Subject: [PATCH 098/141] Implement shutdown_abruptly --- tests/test_server.py | 31 ++++++++++++++++++++++++++++ websocket_server/websocket_server.py | 26 +++++++++++++++++++---- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 2f09012..fed63b5 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,6 +1,7 @@ from utils import session, client_session, server from time import sleep +import websocket import pytest @@ -37,6 +38,36 @@ def test_shutdown_gracefully(client_session): assert not server.clients +def test_shutdown_abruptly(client_session): + client, server = client_session + assert client.ws.sock and client.ws.sock.connected + assert server.socket.fileno() > 0 + + server.shutdown_abruptly() + sleep(0.5) + + # Ensure server socket died + assert server.socket.fileno() == -1 + + # Ensure client handler terminated + assert server.received_messages == [] + assert client.errors == [] + client.ws.send("1st msg after server shutdown") + sleep(0.5) + + # Note the message is received since the client handler + # will terminate only once it has received the last message + # and break out of the keep_alive loop. Any consecutive messages + # will not be received though. + assert server.received_messages == ["1st msg after server shutdown"] + assert len(client.errors) == 1 + assert isinstance(client.errors[0], websocket._exceptions.WebSocketConnectionClosedException) + + # Try to send 2nd message + with pytest.raises(websocket._exceptions.WebSocketConnectionClosedException): + client.ws.send("2nd msg after server shutdown") + + def test_client_closes_gracefully(session): client, server = session assert client.connected diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index b0346b6..0e4ff79 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -86,16 +86,34 @@ def send_message(self, client, msg): def send_message_to_all(self, msg): self._multicast(msg) - def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + def _terminate_client_handlers(self): + """ + Ensures request handler for each client is terminated correctly """ - Close with a websocket handshake + for client in self.clients: + client["handler"].keep_alive = False + client["handler"].finish() + client["handler"].connection.close() - 1. Send CLOSE to all clients - 2. Close TCP + def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + """ + Send a CLOSE handshake to all connected clients before terminating server """ self.keep_alive = False + + # Send CLOSE to clients for client in self.clients: client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) + + self._terminate_client_handlers() + self.server_close() + + def shutdown_abruptly(self): + """ + Terminate server without sending a CLOSE handshake + """ + self.keep_alive = False + self._terminate_client_handlers() self.server_close() From 7286312cb022009672f05fb89ae46413382d9fc0 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:33:07 +0100 Subject: [PATCH 099/141] Cleanup --- websocket_server/websocket_server.py | 58 +++++++++++++++------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 0e4ff79..e637607 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -86,35 +86,11 @@ def send_message(self, client, msg): def send_message_to_all(self, msg): self._multicast(msg) - def _terminate_client_handlers(self): - """ - Ensures request handler for each client is terminated correctly - """ - for client in self.clients: - client["handler"].keep_alive = False - client["handler"].finish() - client["handler"].connection.close() - def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): - """ - Send a CLOSE handshake to all connected clients before terminating server - """ - self.keep_alive = False - - # Send CLOSE to clients - for client in self.clients: - client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) - - self._terminate_client_handlers() - self.server_close() + self._shutdown_gracefully(status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON) def shutdown_abruptly(self): - """ - Terminate server without sending a CLOSE handshake - """ - self.keep_alive = False - self._terminate_client_handlers() - self.server_close() + self._shutdown_abruptly() class WebsocketServer(ThreadingMixIn, TCPServer, API): @@ -190,6 +166,36 @@ def handler_to_client(self, handler): if client['handler'] == handler: return client + def _terminate_client_handlers(self): + """ + Ensures request handler for each client is terminated correctly + """ + for client in self.clients: + client["handler"].keep_alive = False + client["handler"].finish() + client["handler"].connection.close() + + def _shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + """ + Send a CLOSE handshake to all connected clients before terminating server + """ + self.keep_alive = False + + # Send CLOSE to clients + for client in self.clients: + client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) + + self._terminate_client_handlers() + self.server_close() + + def _shutdown_abruptly(self): + """ + Terminate server without sending a CLOSE handshake + """ + self.keep_alive = False + self._terminate_client_handlers() + self.server_close() + class WebSocketHandler(StreamRequestHandler): From 65247bd4149329aa97945bd0f0d528b2924cbe15 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:43:21 +0100 Subject: [PATCH 100/141] Update README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 97ef3f1..e2b3f6f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The WebsocketServer can be initialized with the below parameters. *`key`* - If using SSL, this is the path to the key. -*`cert`* - If using SSL, this is the path to the certificate. +*`cert`* - If using SSL, this is the path to the certificate. ### Properties @@ -78,6 +78,9 @@ The WebsocketServer can be initialized with the below parameters. | `set_fn_message_received()` | Sets a callback function that will be called when a `client` sends a message | function | None | | `send_message()` | Sends a `message` to a specific `client`. The message is a simple string. | client, message | None | | `send_message_to_all()` | Sends a `message` to **all** connected clients. The message is a simple string. | message | None | +| `shutdown_gracefully()` | Shutdown server by sending a websocket CLOSE handshake to all connected clients. | None | None | +| `shutdown_abruptly()` | Shutdown server without sending any websocket CLOSE handshake. | None | None | + ### Callback functions From 768be99a6336a4af79b67c80dc6f440621a7b69d Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:49:30 +0100 Subject: [PATCH 101/141] Bump version to v0.5.4 --- docs/release-workflow.md | 7 ++++--- releases.txt | 3 +++ setup.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/release-workflow.md b/docs/release-workflow.md index 8c0d723..3774902 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -6,8 +6,9 @@ from development comes with a tag. General flow - 1. Update VERSION in setup.py from development branch and commit - 2. Update releases.txt - 3. Merge development into master (`git merge --no-ff development`) + 1. Get in dev branch + 2. Update VERSION in setup.py and releases.txt file + 3. Make a commit + 4. Merge development into master (`git merge --no-ff development`) 4. Add corresponding version as a new tag (`git tag `) e.g. git tag v0.3.0 5. Push everything (`git push --tags && git push`) diff --git a/releases.txt b/releases.txt index 633ac84..bf5ef50 100644 --- a/releases.txt +++ b/releases.txt @@ -4,3 +4,6 @@ 0.5.1 - SSL support - Drop Python 2 support + +0.5.4 +- Add API for shutting down server (abruptly & gracefully) diff --git a/setup.py b/setup.py index 4782866..238d764 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.5.1' +VERSION = '0.5.4' def get_tag_version(): From b5935a6a01df1b653981bd2856f16f57a29901d9 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 17:55:33 +0100 Subject: [PATCH 102/141] Hotfix: Update tox --- .circleci/config.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 65bbd8d..0ff18bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ orbs: jobs: test: docker: - - image: circleci/python:3 + - image: circleci/python:3.7 steps: - checkout - run: diff --git a/tox.ini b/tox.ini index e94ca52..d5dcffa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37 +envlist = python3.5,python3.6,python3.7 [testenv] deps=pytest From f1a86fa892248b525bb9842b85c5dacc95af01cc Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 18:17:13 +0100 Subject: [PATCH 103/141] Don't use tox in CircleCI --- .circleci/config.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 65bbd8d..6942a86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,14 +9,10 @@ jobs: - image: circleci/python:3 steps: - checkout - - run: - name: install dependencies - command: | - sudo pip install tox - run: name: run tests command: | - tox -e py37 + pytest - store_artifacts: path: test-reports destination: test-reports From 29b8706526939e82f3b3bb694e18336570005fe2 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 18:17:13 +0100 Subject: [PATCH 104/141] Don't use tox in CircleCI --- .circleci/config.yml | 4 ++-- README.md | 2 +- requirements.txt | 1 - tox.ini | 7 ------- 4 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index 0ff18bb..4053c63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,11 +12,11 @@ jobs: - run: name: install dependencies command: | - sudo pip install tox + pip install -r requirements.txt - run: name: run tests command: | - tox -e py37 + pytest - store_artifacts: path: test-reports destination: test-reports diff --git a/README.md b/README.md index e2b3f6f..bb3f65a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Testing Run all tests - tox + pytest API diff --git a/requirements.txt b/requirements.txt index 053c4e4..5075893 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ # Dev/test/deploy -tox>=3.24.0 IPython pytest websocket-client>=1.1.1 diff --git a/tox.ini b/tox.ini deleted file mode 100644 index d5dcffa..0000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[tox] -envlist = python3.5,python3.6,python3.7 - -[testenv] -deps=pytest - websocket-client -commands=pytest From 4014980ec391a15d0042d1e3c88d71d14853bb3c Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 18:30:03 +0100 Subject: [PATCH 105/141] Dummy commit --- docs/release-workflow.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-workflow.md b/docs/release-workflow.md index 3774902..316e5d8 100644 --- a/docs/release-workflow.md +++ b/docs/release-workflow.md @@ -12,3 +12,5 @@ General flow 4. Merge development into master (`git merge --no-ff development`) 4. Add corresponding version as a new tag (`git tag `) e.g. git tag v0.3.0 5. Push everything (`git push --tags && git push`) + +- From 54a7fc557f183490e8b09e789c025a16e8b5f986 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 18:59:57 +0100 Subject: [PATCH 106/141] Fix tests not running in circleci --- tests/_bootstrap_.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/_bootstrap_.py b/tests/_bootstrap_.py index b8a7949..646978f 100644 --- a/tests/_bootstrap_.py +++ b/tests/_bootstrap_.py @@ -2,5 +2,5 @@ import sys, os if os.getcwd().endswith('tests'): sys.path.insert(0, '..') -elif os.getcwd().endswith('websocket-server'): +elif os.path.exists('websocket_server'): sys.path.insert(0, '.') From ed73f9c860f6a34655152f0f5125e6d8b855f363 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 3 Oct 2021 19:05:34 +0100 Subject: [PATCH 107/141] Update workflow --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4053c63..68aeaf9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -49,6 +49,7 @@ jobs: command: | tag=`git tag --points-at HEAD` if [ $tag ]; then + echo Uploading python -m twine upload dist/* fi From 032ca904c24c8539a1b70957f2d25ea8cebb1f3e Mon Sep 17 00:00:00 2001 From: Boris Feld Date: Mon, 4 Oct 2021 13:37:09 +0200 Subject: [PATCH 108/141] Add minimum python version in setup.py It looks like latest released version don't support Python 3.5 anymore. It is failing on line https://github.com/Pithikos/python-websocket-server/blob/master/websocket_server/websocket_server.py#L296 because it is using a f-string which is only available in Python 3.6+. If it was intentional, this PR is adding the correct package metadata so pip can choose the right version depending on the user Python Version. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 238d764..779fe38 100644 --- a/setup.py +++ b/setup.py @@ -53,4 +53,5 @@ def run(self): cmdclass={ 'verify': VerifyVersionCommand, }, + python_requires=">=3.6", ) From 7bf69a4d9dbd59b31cbf66a726c3463748c4d5a4 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 17:56:58 +0100 Subject: [PATCH 109/141] Allow running run_forever threaded --- tests/test_server.py | 13 ++++++++++ websocket_server/thread.py | 38 ++++++++++++++++++++++++++++ websocket_server/websocket_server.py | 34 +++++++++++++++++-------- 3 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 websocket_server/thread.py diff --git a/tests/test_server.py b/tests/test_server.py index fed63b5..c55c72d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,5 +1,8 @@ from utils import session, client_session, server from time import sleep +import threading + +from websocket_server import WebsocketServer import websocket import pytest @@ -38,6 +41,16 @@ def test_shutdown_gracefully(client_session): assert not server.clients +def test_run_forever_threaded(): + server = WebsocketServer(port=9999) + assert server.thread == None + + # Run threaded + server.run_forever(threaded=True) + assert server.thread + assert not isinstance(server.thread, threading._MainThread) + + def test_shutdown_abruptly(client_session): client, server = client_session assert client.ws.sock and client.ws.sock.connected diff --git a/websocket_server/thread.py b/websocket_server/thread.py new file mode 100644 index 0000000..a474203 --- /dev/null +++ b/websocket_server/thread.py @@ -0,0 +1,38 @@ +import threading + + +class ThreadWithLoggedException(threading.Thread): + """ + Similar to Thread but will log exceptions to passed logger. + + Args: + logger: Logger instance used to log any exception in child thread + + Exception is also reachable via .exception from the main thread. + """ + + DIVIDER = "*"*80 + + def __init__(self, *args, **kwargs): + try: + self.logger = kwargs.pop("logger") + except KeyError: + raise Exception("Missing 'logger' in kwargs") + super().__init__(*args, **kwargs) + self.exception = None + + def run(self): + try: + if self._target is not None: + self._target(*self._args, **self._kwargs) + except Exception as exception: + thread = threading.current_thread() + self.exception = exception + self.logger.exception(f"{self.DIVIDER}\nException in child thread {thread}: {exception}\n{self.DIVIDER}") + finally: + del self._target, self._args, self._kwargs + + +class WebsocketServerThread(ThreadWithLoggedException): + """Dummy wrapper to make debug messages a bit more readable""" + pass diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index e637607..e82a882 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -9,6 +9,7 @@ import logging from socket import error as SocketError import errno +from websocket_server.thread import WebsocketServerThread from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler @@ -51,16 +52,8 @@ class API(): - def run_forever(self): - try: - logger.info("Listening on port %d for clients.." % self.port) - self.serve_forever() - except KeyboardInterrupt: - self.server_close() - logger.info("Server terminated.") - except Exception as e: - logger.error(str(e), exc_info=True) - exit(1) + def run_forever(self, threaded=False): + return self._run_forever(threaded) def new_client(self, client, server): pass @@ -128,6 +121,27 @@ def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, c self.clients = [] self.id_counter = 0 + self.thread = None + + def _run_forever(self, threaded): + cls_name = self.__class__.__name__ + try: + logger.info("Listening on port %d for clients.." % self.port) + if threaded: + self.daemon = True + self.thread = WebsocketServerThread(target=super().serve_forever, daemon=True, logger=logger) + logger.info(f"Starting {cls_name} on thread {self.thread.getName()}.") + self.thread.start() + else: + self.thread = threading.current_thread() + logger.info(f"Starting {cls_name} on main thread.") + super().serve_forever() + except KeyboardInterrupt: + self.server_close() + logger.info("Server terminated.") + except Exception as e: + logger.error(str(e), exc_info=True) + sys.exit(1) def _message_received_(self, handler, msg): self.message_received(self.handler_to_client(handler), self, msg) From 41721362fc6f53bee0e041b81948ba2b5a498da3 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 18:05:23 +0100 Subject: [PATCH 110/141] Add test class TestServerThreaded --- tests/test_server.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index c55c72d..1956ce1 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -41,14 +41,25 @@ def test_shutdown_gracefully(client_session): assert not server.clients -def test_run_forever_threaded(): - server = WebsocketServer(port=9999) - assert server.thread == None - - # Run threaded - server.run_forever(threaded=True) - assert server.thread - assert not isinstance(server.thread, threading._MainThread) +class TestServerThreaded(): + def test_run_forever(self): + server = WebsocketServer(port=0) + assert server.thread == None + + # Run threaded + server.run_forever(threaded=True) + assert server.thread + assert not isinstance(server.thread, threading._MainThread) + assert server.thread.is_alive() + + def test_shutdown(self): + server = WebsocketServer(port=0) + server.run_forever(threaded=True) + assert server.thread.is_alive() + + # Shutdown de-facto way + server.shutdown() + assert not server.thread.is_alive() def test_shutdown_abruptly(client_session): From 365c2295fa6690ef7dfd7e625a89e1a1830cfbdd Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:15:41 +0100 Subject: [PATCH 111/141] Fix import --- websocket_server/websocket_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index e82a882..916a018 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -9,10 +9,11 @@ import logging from socket import error as SocketError import errno -from websocket_server.thread import WebsocketServerThread - +import threading from socketserver import ThreadingMixIn, TCPServer, StreamRequestHandler +from websocket_server.thread import WebsocketServerThread + logger = logging.getLogger(__name__) logging.basicConfig() From d860948986b21089bb18914b14ad3e6aa8eb51c0 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:25:04 +0100 Subject: [PATCH 112/141] Change fixture server -> threaded_server --- tests/test_server.py | 2 +- tests/utils.py | 24 +++++++++++------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 1956ce1..fdccc12 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,4 +1,4 @@ -from utils import session, client_session, server +from utils import session, client_session, threaded_server from time import sleep import threading diff --git a/tests/utils.py b/tests/utils.py index 2230a51..7dbcdc3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -57,33 +57,31 @@ def handle_received_message(self, client, server, message): @pytest.fixture(scope='function') -def server(): +def threaded_server(): """ Returns the response of a server after""" - s = TestServer(0, loglevel=logging.DEBUG) - server_thread = Thread(target=s.run_forever) - server_thread.daemon = True - server_thread.start() - yield s - s.server_close() + server = TestServer(0, loglevel=logging.DEBUG) + server.run_forever(threaded=True) + yield server + server.server_close() @pytest.fixture -def session(server): +def session(threaded_server): """ Gives a simple connection to a server """ - conn = websocket.create_connection("ws://{}:{}".format(*server.server_address)) - yield conn, server + conn = websocket.create_connection("ws://{}:{}".format(*threaded_server.server_address)) + yield conn, threaded_server conn.close() @pytest.fixture -def client_session(server): +def client_session(threaded_server): """ Gives a TestClient instance connected to a server """ - client = TestClient(port=server.port) + client = TestClient(port=threaded_server.port) sleep(1) assert client.ws.sock and client.ws.sock.connected - yield client, server + yield client, threaded_server client.ws.close() From 68e07a2584455db29d88488fc3a0acda8f367376 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:30:25 +0100 Subject: [PATCH 113/141] Simplify tests --- tests/test_server.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index fdccc12..fb5788d 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -42,24 +42,15 @@ def test_shutdown_gracefully(client_session): class TestServerThreaded(): - def test_run_forever(self): - server = WebsocketServer(port=0) - assert server.thread == None - - # Run threaded - server.run_forever(threaded=True) - assert server.thread - assert not isinstance(server.thread, threading._MainThread) - assert server.thread.is_alive() - - def test_shutdown(self): - server = WebsocketServer(port=0) - server.run_forever(threaded=True) - assert server.thread.is_alive() - - # Shutdown de-facto way - server.shutdown() - assert not server.thread.is_alive() + def test_run_forever(self, threaded_server): + assert threaded_server.thread + assert not isinstance(threaded_server.thread, threading._MainThread) + assert threaded_server.thread.is_alive() + + def test_shutdown(self, threaded_server): + assert threaded_server.thread.is_alive() + threaded_server.shutdown() + assert not threaded_server.thread.is_alive() def test_shutdown_abruptly(client_session): From e77b4af1ff659cd4d13b04d3a01aac1afa5f8926 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:41:14 +0100 Subject: [PATCH 114/141] Fix shutdown when no client connected --- tests/test_server.py | 20 +++++++++++++++++++- websocket_server/websocket_server.py | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index fb5788d..02d3f69 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -41,7 +41,7 @@ def test_shutdown_gracefully(client_session): assert not server.clients -class TestServerThreaded(): +class TestServerThreadedWithoutClient(): def test_run_forever(self, threaded_server): assert threaded_server.thread assert not isinstance(threaded_server.thread, threading._MainThread) @@ -49,9 +49,27 @@ def test_run_forever(self, threaded_server): def test_shutdown(self, threaded_server): assert threaded_server.thread.is_alive() + + # Shutdown de-facto way + # REF: https://docs.python.org/3/library/socketserver.html + # "Tell the serve_forever() loop to stop and + # wait until it does. shutdown() must be called while serve_forever() + # is running in a different thread otherwise it will deadlock." threaded_server.shutdown() assert not threaded_server.thread.is_alive() + def test_shutdown_gracefully_without_clients(self, threaded_server): + assert threaded_server.thread.is_alive() + threaded_server.shutdown_gracefully() + assert not threaded_server.thread.is_alive() + assert threaded_server.socket.fileno() <= 0 + + def test_shutdown_abruptly_without_clients(self, threaded_server): + assert threaded_server.thread.is_alive() + threaded_server.shutdown_abruptly() + assert not threaded_server.thread.is_alive() + assert threaded_server.socket.fileno() <= 0 + def test_shutdown_abruptly(client_session): client, server = client_session diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 916a018..6d97463 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -202,6 +202,7 @@ def _shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_ self._terminate_client_handlers() self.server_close() + self.shutdown() def _shutdown_abruptly(self): """ @@ -210,6 +211,7 @@ def _shutdown_abruptly(self): self.keep_alive = False self._terminate_client_handlers() self.server_close() + self.shutdown() class WebSocketHandler(StreamRequestHandler): From 50067a4c6805ee80eed1515d2638d71518b3c599 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:49:30 +0100 Subject: [PATCH 115/141] Restructure tests --- tests/test_server.py | 152 +++++++++++++++++++++---------------------- 1 file changed, 75 insertions(+), 77 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 02d3f69..d568b75 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -8,39 +8,6 @@ import pytest -def test_send_close(client_session): - """ - Ensure client stops receiving data once we send_close (socket is still open) - """ - client, server = client_session - assert client.received_messages == [] - - server.send_message_to_all("test1") - sleep(0.5) - assert client.received_messages == ["test1"] - - # After CLOSE, client should not be receiving any messages - server.clients[-1]["handler"].send_close() - sleep(0.5) - server.send_message_to_all("test2") - sleep(0.5) - assert client.received_messages == ["test1"] - - -def test_shutdown_gracefully(client_session): - client, server = client_session - assert client.ws.sock and client.ws.sock.connected - assert server.socket.fileno() > 0 - - server.shutdown_gracefully() - sleep(0.5) - - # Ensure all parties disconnected - assert not client.ws.sock - assert server.socket.fileno() == -1 - assert not server.clients - - class TestServerThreadedWithoutClient(): def test_run_forever(self, threaded_server): assert threaded_server.thread @@ -71,47 +38,78 @@ def test_shutdown_abruptly_without_clients(self, threaded_server): assert threaded_server.socket.fileno() <= 0 -def test_shutdown_abruptly(client_session): - client, server = client_session - assert client.ws.sock and client.ws.sock.connected - assert server.socket.fileno() > 0 - - server.shutdown_abruptly() - sleep(0.5) - - # Ensure server socket died - assert server.socket.fileno() == -1 - - # Ensure client handler terminated - assert server.received_messages == [] - assert client.errors == [] - client.ws.send("1st msg after server shutdown") - sleep(0.5) - - # Note the message is received since the client handler - # will terminate only once it has received the last message - # and break out of the keep_alive loop. Any consecutive messages - # will not be received though. - assert server.received_messages == ["1st msg after server shutdown"] - assert len(client.errors) == 1 - assert isinstance(client.errors[0], websocket._exceptions.WebSocketConnectionClosedException) - - # Try to send 2nd message - with pytest.raises(websocket._exceptions.WebSocketConnectionClosedException): - client.ws.send("2nd msg after server shutdown") - - -def test_client_closes_gracefully(session): - client, server = session - assert client.connected - assert server.clients - old_client_handler = server.clients[0]["handler"] - client.close() - assert not client.connected - - # Ensure server closed connection. - # We test this by having the server trying to send - # data to the client - assert not server.clients - with pytest.raises(BrokenPipeError): - old_client_handler.connection.send(b"test") +class TestServerThreadedWithClient(): + def test_send_close(self, client_session): + """ + Ensure client stops receiving data once we send_close (socket is still open) + """ + client, server = client_session + assert client.received_messages == [] + + server.send_message_to_all("test1") + sleep(0.5) + assert client.received_messages == ["test1"] + + # After CLOSE, client should not be receiving any messages + server.clients[-1]["handler"].send_close() + sleep(0.5) + server.send_message_to_all("test2") + sleep(0.5) + assert client.received_messages == ["test1"] + + def test_shutdown_gracefully(self, client_session): + client, server = client_session + assert client.ws.sock and client.ws.sock.connected + assert server.socket.fileno() > 0 + + server.shutdown_gracefully() + sleep(0.5) + + # Ensure all parties disconnected + assert not client.ws.sock + assert server.socket.fileno() == -1 + assert not server.clients + + def test_shutdown_abruptly(self, client_session): + client, server = client_session + assert client.ws.sock and client.ws.sock.connected + assert server.socket.fileno() > 0 + + server.shutdown_abruptly() + sleep(0.5) + + # Ensure server socket died + assert server.socket.fileno() == -1 + + # Ensure client handler terminated + assert server.received_messages == [] + assert client.errors == [] + client.ws.send("1st msg after server shutdown") + sleep(0.5) + + # Note the message is received since the client handler + # will terminate only once it has received the last message + # and break out of the keep_alive loop. Any consecutive messages + # will not be received though. + assert server.received_messages == ["1st msg after server shutdown"] + assert len(client.errors) == 1 + assert isinstance(client.errors[0], websocket._exceptions.WebSocketConnectionClosedException) + + # Try to send 2nd message + with pytest.raises(websocket._exceptions.WebSocketConnectionClosedException): + client.ws.send("2nd msg after server shutdown") + + def test_client_closes_gracefully(self, session): + client, server = session + assert client.connected + assert server.clients + old_client_handler = server.clients[0]["handler"] + client.close() + assert not client.connected + + # Ensure server closed connection. + # We test this by having the server trying to send + # data to the client + assert not server.clients + with pytest.raises(BrokenPipeError): + old_client_handler.connection.send(b"test") From afe2c117c6b0edd22dc6fe18ba46245adbe75747 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 19:54:22 +0100 Subject: [PATCH 116/141] Update documentation --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e2b3f6f..672b5bc 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ The WebsocketServer can be initialized with the below parameters. | Method | Description | Takes | Gives | |-----------------------------|---------------------------------------------------------------------------------------|-----------------|-------| +| `run_forever()` | Runs server until shutdown_gracefully or shutdown_abruptly are called. | threaded: run server on its own thread if True | None | | `set_fn_new_client()` | Sets a callback function that will be called for every new `client` connecting to us | function | None | | `set_fn_client_left()` | Sets a callback function that will be called for every `client` disconnecting from us | function | None | | `set_fn_message_received()` | Sets a callback function that will be called when a `client` sends a message | function | None | From 83f9e0244ce53b152092a1563cb99aed682fb7d2 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 20:39:38 +0100 Subject: [PATCH 117/141] Bump to v0.5.5 --- releases.txt | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/releases.txt b/releases.txt index bf5ef50..71d170f 100644 --- a/releases.txt +++ b/releases.txt @@ -7,3 +7,7 @@ 0.5.4 - Add API for shutting down server (abruptly & gracefully) + +0.5.5 +- Allow running run_forever threaded +- Fix shutting down of a server without connected clients diff --git a/setup.py b/setup.py index 238d764..fe18fb6 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.5.4' +VERSION = '0.5.5' def get_tag_version(): From 515750808f47dd48dc62566ea6e8c120f1d82e92 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 20:42:28 +0100 Subject: [PATCH 118/141] Update test --- tests/test_text_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_text_messages.py b/tests/test_text_messages.py index 28dd4e6..7f93ff5 100644 --- a/tests/test_text_messages.py +++ b/tests/test_text_messages.py @@ -1,4 +1,4 @@ -from utils import session, server +from utils import session, threaded_server def test_text_message_of_length_1(session): From 9b8f655cea0e8ac4205c2e667e6972cd119b993d Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 9 Oct 2021 20:59:53 +0100 Subject: [PATCH 119/141] Support from py3.6+ --- README.md | 2 +- releases.txt | 3 +++ setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a59a640..3b22a76 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Websocket Server A minimal Websockets Server in Python with no external dependencies. - * Python3.5+ + * Python3.6+ * Clean simple API * Multiple clients * No dependencies diff --git a/releases.txt b/releases.txt index 71d170f..3be8dc9 100644 --- a/releases.txt +++ b/releases.txt @@ -11,3 +11,6 @@ 0.5.5 - Allow running run_forever threaded - Fix shutting down of a server without connected clients + +0.5.6 +- Support from Python3.6+ diff --git a/setup.py b/setup.py index d25e946..122ae8d 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.5.5' +VERSION = '0.5.6' def get_tag_version(): From 77f6dcee54209030940dfe7286a9facb92307e59 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:01:41 +0100 Subject: [PATCH 120/141] Add attribute 'host' to server --- tests/test_server.py | 5 +++++ websocket_server/websocket_server.py | 1 + 2 files changed, 6 insertions(+) diff --git a/tests/test_server.py b/tests/test_server.py index d568b75..b2f5dd6 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -14,6 +14,11 @@ def test_run_forever(self, threaded_server): assert not isinstance(threaded_server.thread, threading._MainThread) assert threaded_server.thread.is_alive() + def test_attributes(self, threaded_server): + tpl = threaded_server.server_address + assert threaded_server.port == tpl[1] + assert threaded_server.host == tpl[0] + def test_shutdown(self, threaded_server): assert threaded_server.thread.is_alive() diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 6d97463..4e04a80 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -115,6 +115,7 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, cert=None): logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) + self.host = host self.port = self.socket.getsockname()[1] self.key = key From bea9dc8453cdebf7c92e331d7b1fb8c44ac5a900 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:02:07 +0100 Subject: [PATCH 121/141] Set server to use port 0 by default --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 4e04a80..3c5bc06 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -112,7 +112,7 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): allow_reuse_address = True daemon_threads = True # comment to keep threads alive until finished - def __init__(self, port, host='127.0.0.1', loglevel=logging.WARNING, key=None, cert=None): + def __init__(self, port=0, host='127.0.0.1', loglevel=logging.WARNING, key=None, cert=None): logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) self.host = host From a1f75994840781309752f452fae822f35b841508 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:02:53 +0100 Subject: [PATCH 122/141] Reverse order of params host and port --- README.md | 4 ++-- tests/test_message_lengths.py | 2 +- tests/utils.py | 2 +- websocket_server/websocket_server.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 672b5bc..a2b8e4f 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ from websocket_server import WebsocketServer def new_client(client, server): server.send_message_to_all("Hey all, a new client has joined us") -server = WebsocketServer(13254, host='127.0.0.1', loglevel=logging.INFO) +server = WebsocketServer(host='127.0.0.1', port=13254, loglevel=logging.INFO) server.set_fn_new_client(new_client) server.run_forever() ```` @@ -116,7 +116,7 @@ from websocket_server import WebsocketServer def new_client(client, server): server.send_message_to_all("Hey all, a new client has joined us") -server = WebsocketServer(13254, host='127.0.0.1', loglevel=logging.INFO, key="key.pem", cert="cert.pem") +server = WebsocketServer(host='127.0.0.1', port=13254, loglevel=logging.INFO, key="key.pem", cert="cert.pem") server.set_fn_new_client(new_client) server.run_forever() ```` diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py index 14fec70..d8a4cdd 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_message_lengths.py @@ -12,7 +12,7 @@ @pytest.fixture(scope='function') def server(): """ Returns the response of a server after""" - s = WebsocketServer(0, loglevel=logging.DEBUG) + s = WebsocketServer(loglevel=logging.DEBUG) server_thread = Thread(target=s.run_forever) server_thread.daemon = True server_thread.start() diff --git a/tests/utils.py b/tests/utils.py index 7dbcdc3..028b880 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -59,7 +59,7 @@ def handle_received_message(self, client, server, message): @pytest.fixture(scope='function') def threaded_server(): """ Returns the response of a server after""" - server = TestServer(0, loglevel=logging.DEBUG) + server = TestServer(loglevel=logging.DEBUG) server.run_forever(threaded=True) yield server server.server_close() diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 3c5bc06..1bed1e8 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -112,7 +112,7 @@ class WebsocketServer(ThreadingMixIn, TCPServer, API): allow_reuse_address = True daemon_threads = True # comment to keep threads alive until finished - def __init__(self, port=0, host='127.0.0.1', loglevel=logging.WARNING, key=None, cert=None): + def __init__(self, host='127.0.0.1', port=0, loglevel=logging.WARNING, key=None, cert=None): logger.setLevel(loglevel) TCPServer.__init__(self, (host, port), WebSocketHandler) self.host = host From bf73382e17d334790a0b54d477577d297623f464 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:10:36 +0100 Subject: [PATCH 123/141] Refactor message tests to use existing pytest fixtures --- tests/test_message_lengths.py | 59 ++++++++++------------------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py index d8a4cdd..b8c2c5a 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_message_lengths.py @@ -1,96 +1,71 @@ -from time import sleep -import logging -from threading import Thread - -import pytest -from websocket import create_connection # websocket-client - import _bootstrap_ -from websocket_server import WebsocketServer - - -@pytest.fixture(scope='function') -def server(): - """ Returns the response of a server after""" - s = WebsocketServer(loglevel=logging.DEBUG) - server_thread = Thread(target=s.run_forever) - server_thread.daemon = True - server_thread.start() - yield s - s.server_close() - - -@pytest.fixture -def session(server): - ws = create_connection("ws://{}:{}".format(*server.server_address)) - yield ws, server - ws.close() +from utils import session, threaded_server def test_text_message_of_length_1(session): - client, server = session + conn, server = session server.send_message_to_all('$') - assert client.recv() == '$' + assert conn.recv() == '$' def test_text_message_of_length_125B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqr125' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_126B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrs126' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_127B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrst127' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_208B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw208' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_1251B(session): - client, server = session + conn, server = session msg = ('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqr125'*10)+'1' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_68KB(session): - client, server = session + conn, server = session msg = '$'+('a'*67993)+'68000'+'^' assert len(msg) == 68000 server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_1500KB(session): """ An enormous message (well beyond 65K) """ - client, server = session + conn, server = session msg = '$'+('a'*1499991)+'1500000'+'^' assert len(msg) == 1500000 server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg From bcef2715bd9ea220a19d241ef47db0a867bbd48b Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:17:33 +0100 Subject: [PATCH 124/141] Refactor test_text_messages to use existing fixtures --- tests/test_text_messages.py | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/test_text_messages.py b/tests/test_text_messages.py index 28dd4e6..29e771c 100644 --- a/tests/test_text_messages.py +++ b/tests/test_text_messages.py @@ -1,89 +1,89 @@ -from utils import session, server +from utils import session, threaded_server def test_text_message_of_length_1(session): - client, server = session + conn, server = session server.send_message_to_all('$') - assert client.recv() == '$' + assert conn.recv() == '$' def test_text_message_of_length_125B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqr125' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_126B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrs126' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_127B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrst127' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_208B(session): - client, server = session + conn, server = session msg = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvw208' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_1251B(session): - client, server = session + conn, server = session msg = ('abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'\ 'abcdefghijklmnopqr125'*10)+'1' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_68KB(session): - client, server = session + conn, server = session msg = '$'+('a'*67993)+'68000'+'^' assert len(msg) == 68000 server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_of_length_1500KB(session): """ An enormous message (well beyond 65K) """ - client, server = session + conn, server = session msg = '$'+('a'*1499991)+'1500000'+'^' assert len(msg) == 1500000 server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_with_unicode_characters(session): - client, server = session + conn, server = session msg = '$äüö^' server.send_message_to_all(msg) - assert client.recv() == msg + assert conn.recv() == msg def test_text_message_stress_bursts(session): - """ Scenario: server sends multiple different message to the same client + """ Scenario: server sends multiple different message to the same conn at once """ from threading import Thread NUM_THREADS = 100 MESSAGE_LEN = 1000 - client, server = session + conn, server = session messages_received = [] # Threads receing @@ -91,7 +91,7 @@ def test_text_message_stress_bursts(session): for i in range(NUM_THREADS): th = Thread( target=lambda fn: messages_received.append(fn()), - args=(client.recv,) + args=(conn.recv,) ) th.daemon = True threads_receiving.append(th) From b0e6a37e874df3468864b0a0f1b957168f288f7e Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:20:12 +0100 Subject: [PATCH 125/141] Update release notes --- releases.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/releases.txt b/releases.txt index 3be8dc9..f143b39 100644 --- a/releases.txt +++ b/releases.txt @@ -14,3 +14,7 @@ 0.5.6 - Support from Python3.6+ + +0.6.0 +- Change order of params 'host' and 'port' +- Add host attribute to server From 4ca9dcd777f289001ae0c2795b05fce8150403e4 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:23:08 +0100 Subject: [PATCH 126/141] Refactor test-suite to use conftest --- tests/{utils.py => conftest.py} | 0 tests/test_message_lengths.py | 4 ---- tests/test_server.py | 3 --- tests/test_text_messages.py | 3 --- 4 files changed, 10 deletions(-) rename tests/{utils.py => conftest.py} (100%) diff --git a/tests/utils.py b/tests/conftest.py similarity index 100% rename from tests/utils.py rename to tests/conftest.py diff --git a/tests/test_message_lengths.py b/tests/test_message_lengths.py index b8c2c5a..03f9d96 100644 --- a/tests/test_message_lengths.py +++ b/tests/test_message_lengths.py @@ -1,7 +1,3 @@ -import _bootstrap_ -from utils import session, threaded_server - - def test_text_message_of_length_1(session): conn, server = session server.send_message_to_all('$') diff --git a/tests/test_server.py b/tests/test_server.py index b2f5dd6..447d823 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,9 +1,6 @@ -from utils import session, client_session, threaded_server from time import sleep import threading -from websocket_server import WebsocketServer - import websocket import pytest diff --git a/tests/test_text_messages.py b/tests/test_text_messages.py index 29e771c..b4f2d0f 100644 --- a/tests/test_text_messages.py +++ b/tests/test_text_messages.py @@ -1,6 +1,3 @@ -from utils import session, threaded_server - - def test_text_message_of_length_1(session): conn, server = session server.send_message_to_all('$') From 1df6cd8cdf38b4e9f2e014fa8a53251d1324aede Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:24:46 +0100 Subject: [PATCH 127/141] Obliterate bootstrap file --- tests/_bootstrap_.py | 6 ------ tests/conftest.py | 7 ++++++- tests/test_handshake.py | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 tests/_bootstrap_.py diff --git a/tests/_bootstrap_.py b/tests/_bootstrap_.py deleted file mode 100644 index 646978f..0000000 --- a/tests/_bootstrap_.py +++ /dev/null @@ -1,6 +0,0 @@ -# Add path to source code -import sys, os -if os.getcwd().endswith('tests'): - sys.path.insert(0, '..') -elif os.path.exists('websocket_server'): - sys.path.insert(0, '.') diff --git a/tests/conftest.py b/tests/conftest.py index 028b880..d8ddf26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,12 @@ import pytest import websocket # websocket-client -import _bootstrap_ +# Add path to source code +import sys, os +if os.getcwd().endswith('tests'): + sys.path.insert(0, '..') +elif os.path.exists('websocket_server'): + sys.path.insert(0, '.') from websocket_server import WebsocketServer diff --git a/tests/test_handshake.py b/tests/test_handshake.py index dfbcc36..74ace26 100644 --- a/tests/test_handshake.py +++ b/tests/test_handshake.py @@ -1,4 +1,3 @@ -import _bootstrap_ from websocket_server import WebSocketHandler From cf487b777373e305308c6e3052e77cb7b75dfd10 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 23 Oct 2021 11:28:24 +0100 Subject: [PATCH 128/141] Bump version to 0.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 122ae8d..e345f7e 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.5.6' +VERSION = '0.6.0' def get_tag_version(): From 6a64314f7e54fa89df8502e5211d96e56576940a Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 15 Nov 2021 10:08:11 +0000 Subject: [PATCH 129/141] Add lock to all socket.send --- websocket_server/websocket_server.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 1bed1e8..a260987 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -219,6 +219,8 @@ class WebSocketHandler(StreamRequestHandler): def __init__(self, socket, addr, server): self.server = server + assert not hasattr(self, "_send_lock"), "_send_lock already exists" + self._send_lock = threading.Lock() if server.key and server.cert: try: socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) @@ -321,7 +323,8 @@ def send_close(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): # Send CLOSE with status & reason header.append(FIN | OPCODE_CLOSE_CONN) header.append(payload_length) - self.request.send(header + payload) + with self._send_lock: + self.request.send(header + payload) def send_text(self, message, opcode=OPCODE_TEXT): """ @@ -364,7 +367,8 @@ def send_text(self, message, opcode=OPCODE_TEXT): raise Exception("Message is too big. Consider breaking it into chunks.") return - self.request.send(header + payload) + with self._send_lock: + self.request.send(header + payload) def read_http_headers(self): headers = {} @@ -397,7 +401,8 @@ def handshake(self): return response = self.make_handshake_response(key) - self.handshake_done = self.request.send(response.encode()) + with self._send_lock: + self.handshake_done = self.request.send(response.encode()) self.valid_client = True self.server._new_client_(self) From c7b146717aa9b5e0c45e575e4d135a6f2fcb8f34 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 15 Nov 2021 10:12:06 +0000 Subject: [PATCH 130/141] Bump version --- releases.txt | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/releases.txt b/releases.txt index f143b39..27255f2 100644 --- a/releases.txt +++ b/releases.txt @@ -18,3 +18,6 @@ 0.6.0 - Change order of params 'host' and 'port' - Add host attribute to server + +0.6.1 +- Sending data is now thread-safe diff --git a/setup.py b/setup.py index e345f7e..5b9b8f7 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.6.0' +VERSION = '0.6.1' def get_tag_version(): From aaebb5d03b162dfb3c2d60ec082ab6f27241d330 Mon Sep 17 00:00:00 2001 From: Manos Date: Mon, 15 Nov 2021 10:21:21 +0000 Subject: [PATCH 131/141] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d60f3f8..f8cdfe7 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ A minimal Websockets Server in Python with no external dependencies. * Multiple clients * No dependencies -Notice that this implementation does not support the more advanced features -like multithreading etc. The project is focused mainly on making it easy to run a -websocket server for prototyping, testing or for making a GUI for your application. +Notice this project is focused mainly on making it easy to run a websocket server for prototyping, testing or for making a GUI for your application. Thus not all possible features of Websockets are supported. Installation From 2f97a44272a0208005993a6152dd0a7671e31b07 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 27 Nov 2021 10:56:00 +0000 Subject: [PATCH 132/141] Add API to disconnect clients --- README.md | 6 ++++-- tests/test_server.py | 26 +++++++++++++++++++++++++ websocket_server/websocket_server.py | 29 +++++++++++++++++++++------- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d60f3f8..343b69c 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,10 @@ The WebsocketServer can be initialized with the below parameters. | `set_fn_message_received()` | Sets a callback function that will be called when a `client` sends a message | function | None | | `send_message()` | Sends a `message` to a specific `client`. The message is a simple string. | client, message | None | | `send_message_to_all()` | Sends a `message` to **all** connected clients. The message is a simple string. | message | None | -| `shutdown_gracefully()` | Shutdown server by sending a websocket CLOSE handshake to all connected clients. | None | None | -| `shutdown_abruptly()` | Shutdown server without sending any websocket CLOSE handshake. | None | None | +| `disconnect_clients_gracefully()` | Disconnect all connected clients by sending a websocket CLOSE handshake. | Optional: status, reason | None | +| `disconnect_clients_abruptly()` | Disconnect all connected clients. Clients won't be aware until they try to send some data. | None | None | +| `shutdown_gracefully()` | Disconnect clients with a CLOSE handshake and shutdown server. | Optional: status, reason | None | +| `shutdown_abruptly()` | Disconnect clients and shutdown server with no handshake. | None | None | diff --git a/tests/test_server.py b/tests/test_server.py index 447d823..801e555 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -115,3 +115,29 @@ def test_client_closes_gracefully(self, session): assert not server.clients with pytest.raises(BrokenPipeError): old_client_handler.connection.send(b"test") + + def test_disconnect_clients_abruptly(self, session): + client, server = session + assert client.connected + assert server.clients + server.disconnect_clients_abruptly() + assert not server.clients + + # Client won't be aware until trying to write more data + with pytest.raises(BrokenPipeError): + for i in range(3): + client.send("test") + sleep(0.2) + + def test_disconnect_clients_gracefully(self, session): + client, server = session + assert client.connected + assert server.clients + server.disconnect_clients_gracefully() + assert not server.clients + + # Client won't be aware until trying to write more data + with pytest.raises(BrokenPipeError): + for i in range(3): + client.send("test") + sleep(0.2) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index a260987..ebd8857 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -86,6 +86,12 @@ def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_R def shutdown_abruptly(self): self._shutdown_abruptly() + def disconnect_clients_gracefully(self): + self._disconnect_clients_gracefully() + + def disconnect_clients_abruptly(self): + self._disconnect_clients_abruptly() + class WebsocketServer(ThreadingMixIn, TCPServer, API): """ @@ -196,12 +202,7 @@ def _shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_ Send a CLOSE handshake to all connected clients before terminating server """ self.keep_alive = False - - # Send CLOSE to clients - for client in self.clients: - client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) - - self._terminate_client_handlers() + self._disconnect_clients_gracefully(status, reason) self.server_close() self.shutdown() @@ -210,10 +211,24 @@ def _shutdown_abruptly(self): Terminate server without sending a CLOSE handshake """ self.keep_alive = False - self._terminate_client_handlers() + self._disconnect_clients_abruptly() self.server_close() self.shutdown() + def _disconnect_clients_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + """ + Terminate clients gracefully without shutting down the server + """ + for client in self.clients: + client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) + self._terminate_client_handlers() + + def _disconnect_clients_abruptly(self): + """ + Terminate clients abruptly (no CLOSE handshake) without shutting down the server + """ + self._terminate_client_handlers() + class WebSocketHandler(StreamRequestHandler): From b58b4c284e5614e6dc112a939f136e4e0abd1945 Mon Sep 17 00:00:00 2001 From: Mano Date: Sat, 27 Nov 2021 11:01:52 +0000 Subject: [PATCH 133/141] Bump release --- releases.txt | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/releases.txt b/releases.txt index 27255f2..59e1806 100644 --- a/releases.txt +++ b/releases.txt @@ -21,3 +21,6 @@ 0.6.1 - Sending data is now thread-safe + +0.6.2 +- Add API for disconnecting clients diff --git a/setup.py b/setup.py index 5b9b8f7..b44a237 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.6.1' +VERSION = '0.6.2' def get_tag_version(): From 530c0ebf270485a3188007023fea2fbc7c914e7c Mon Sep 17 00:00:00 2001 From: "Zhu S.R" <67990189+MuRongPIG@users.noreply.github.com> Date: Fri, 3 Dec 2021 21:10:38 +0800 Subject: [PATCH 134/141] Update server.py --- server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server.py b/server.py index f0587c6..210c884 100644 --- a/server.py +++ b/server.py @@ -19,7 +19,7 @@ def message_received(client, server, message): PORT=9001 -server = WebsocketServer(PORT) +server = WebsocketServer(port = PORT) server.set_fn_new_client(new_client) server.set_fn_client_left(client_left) server.set_fn_message_received(message_received) From dd31e7673e8ceef52377b1cd44f627ddb76559ee Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 13 Dec 2021 10:40:08 +0000 Subject: [PATCH 135/141] Remove DeprecationWarning for using warn instead of warning in logs --- websocket_server/websocket_server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index ebd8857..6d78613 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -240,7 +240,7 @@ def __init__(self, socket, addr, server): try: socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) except: # Not sure which exception it throws if the key/cert isn't found - logger.warn("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) + logger.warning("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) StreamRequestHandler.__init__(self, socket, addr, server) def setup(self): @@ -281,14 +281,14 @@ def read_next_message(self): self.keep_alive = 0 return if not masked: - logger.warn("Client must always be masked.") + logger.warning("Client must always be masked.") self.keep_alive = 0 return if opcode == OPCODE_CONTINUATION: - logger.warn("Continuation frames are not supported.") + logger.warning("Continuation frames are not supported.") return elif opcode == OPCODE_BINARY: - logger.warn("Binary frames are not supported.") + logger.warning("Binary frames are not supported.") return elif opcode == OPCODE_TEXT: opcode_handler = self.server._message_received_ @@ -297,7 +297,7 @@ def read_next_message(self): elif opcode == OPCODE_PONG: opcode_handler = self.server._pong_received_ else: - logger.warn("Unknown opcode %#x." % opcode) + logger.warning("Unknown opcode %#x." % opcode) self.keep_alive = 0 return From b1cc12e026d13fe175e471f0f4ac42cdeea8a8c9 Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 13 Dec 2021 10:42:03 +0000 Subject: [PATCH 136/141] Bump up version --- releases.txt | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/releases.txt b/releases.txt index 59e1806..8b15c9b 100644 --- a/releases.txt +++ b/releases.txt @@ -24,3 +24,6 @@ 0.6.2 - Add API for disconnecting clients + +0.6.3 +- Remove deprecation warnings diff --git a/setup.py b/setup.py index b44a237..baf1184 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.6.2' +VERSION = '0.6.3' def get_tag_version(): From edd0e20987a95b6b506808f2144345d63d1fa0b9 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 19 Dec 2021 15:38:13 +0000 Subject: [PATCH 137/141] Fix disconnect_clients_gracefully to take params 'status' and 'reason' --- websocket_server/websocket_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 6d78613..2b1c49a 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -81,13 +81,13 @@ def send_message_to_all(self, msg): self._multicast(msg) def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): - self._shutdown_gracefully(status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON) + self._shutdown_gracefully(status, reason) def shutdown_abruptly(self): self._shutdown_abruptly() - def disconnect_clients_gracefully(self): - self._disconnect_clients_gracefully() + def disconnect_clients_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + self._disconnect_clients_gracefully(status, reason) def disconnect_clients_abruptly(self): self._disconnect_clients_abruptly() From 531624a4f04e2ff8a96ace12bcbebad6b71b019d Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 19 Dec 2021 16:14:56 +0000 Subject: [PATCH 138/141] Fix shutdown_gracefully always using CLOSE_STATUS_NORMAL --- websocket_server/websocket_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 2b1c49a..a5dcaa7 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -220,7 +220,7 @@ def _disconnect_clients_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFA Terminate clients gracefully without shutting down the server """ for client in self.clients: - client["handler"].send_close(CLOSE_STATUS_NORMAL, reason) + client["handler"].send_close(status, reason) self._terminate_client_handlers() def _disconnect_clients_abruptly(self): From 96a5a85b96f529b9dfa35b5b357dd7a3a9cac3d6 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 19 Dec 2021 16:24:28 +0000 Subject: [PATCH 139/141] Add deny_new_connections and allow_new_connections --- README.md | 7 +++--- tests/test_server.py | 16 ++++++++++++++ websocket_server/websocket_server.py | 33 +++++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 343b69c..eb20d43 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,10 @@ The WebsocketServer can be initialized with the below parameters. | `send_message_to_all()` | Sends a `message` to **all** connected clients. The message is a simple string. | message | None | | `disconnect_clients_gracefully()` | Disconnect all connected clients by sending a websocket CLOSE handshake. | Optional: status, reason | None | | `disconnect_clients_abruptly()` | Disconnect all connected clients. Clients won't be aware until they try to send some data. | None | None | -| `shutdown_gracefully()` | Disconnect clients with a CLOSE handshake and shutdown server. | Optional: status, reason | None | -| `shutdown_abruptly()` | Disconnect clients and shutdown server with no handshake. | None | None | - +| `shutdown_gracefully()` | Disconnect clients with a CLOSE handshake and shutdown server. | Optional: status, reason | None | +| `shutdown_abruptly()` | Disconnect clients and shutdown server with no handshake. | None | None | +| `deny_new_connections()` | Close connection for new clients. | Optional: status, reason | None | +| `allow_new_connections()` | Allows back connection for new clients. | | None | ### Callback functions diff --git a/tests/test_server.py b/tests/test_server.py index 801e555..d03ce08 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -141,3 +141,19 @@ def test_disconnect_clients_gracefully(self, session): for i in range(3): client.send("test") sleep(0.2) + + def test_deny_new_connections(self, threaded_server): + url = "ws://{}:{}".format(*threaded_server.server_address) + server = threaded_server + server.deny_new_connections(status=1013, reason=b"Please try re-connecting later") + + conn = websocket.create_connection(url) + try: + conn.send("test") + except websocket.WebSocketProtocolException as e: + assert 'Invalid close opcode' in e.args[0] + assert not server.clients + + server.allow_new_connections() + conn = websocket.create_connection(url) + conn.send("test") diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index a5dcaa7..083ee17 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -80,6 +80,12 @@ def send_message(self, client, msg): def send_message_to_all(self, msg): self._multicast(msg) + def deny_new_connections(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): + self._deny_new_connections(status, reason) + + def allow_new_connections(self): + self._allow_new_connections() + def shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): self._shutdown_gracefully(status, reason) @@ -131,6 +137,8 @@ def __init__(self, host='127.0.0.1', port=0, loglevel=logging.WARNING, key=None, self.id_counter = 0 self.thread = None + self._deny_clients = False + def _run_forever(self, threaded): cls_name = self.__class__.__name__ try: @@ -161,6 +169,13 @@ def _pong_received_(self, handler, msg): pass def _new_client_(self, handler): + if self._deny_clients: + status = self._deny_clients["status"] + reason = self._deny_clients["reason"] + handler.send_close(status, reason) + self._terminate_client_handler(handler) + return + self.id_counter += 1 client = { 'id': self.id_counter, @@ -188,14 +203,17 @@ def handler_to_client(self, handler): if client['handler'] == handler: return client + def _terminate_client_handler(self, handler): + handler.keep_alive = False + handler.finish() + handler.connection.close() + def _terminate_client_handlers(self): """ Ensures request handler for each client is terminated correctly """ for client in self.clients: - client["handler"].keep_alive = False - client["handler"].finish() - client["handler"].connection.close() + self._terminate_client_handler(client["handler"]) def _shutdown_gracefully(self, status=CLOSE_STATUS_NORMAL, reason=DEFAULT_CLOSE_REASON): """ @@ -229,6 +247,15 @@ def _disconnect_clients_abruptly(self): """ self._terminate_client_handlers() + def _deny_new_connections(self, status, reason): + self._deny_clients = { + "status": status, + "reason": reason, + } + + def _allow_new_connections(self): + self._deny_clients = False + class WebSocketHandler(StreamRequestHandler): From 3b860611747d49fe8c61316b74e2ea5ed92421b7 Mon Sep 17 00:00:00 2001 From: Mano Date: Sun, 19 Dec 2021 16:33:16 +0000 Subject: [PATCH 140/141] Bump version --- releases.txt | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/releases.txt b/releases.txt index 8b15c9b..2fda06c 100644 --- a/releases.txt +++ b/releases.txt @@ -27,3 +27,8 @@ 0.6.3 - Remove deprecation warnings + +0.6.4 +- Add deny_new_connections & allow_new_connections +- Fix disconnect_clients_gracefully to now take params +- Fix shutdown_gracefully unused param diff --git a/setup.py b/setup.py index baf1184..684b88d 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ from distutils.command.install import install -VERSION = '0.6.3' +VERSION = '0.6.4' def get_tag_version(): From fb81db184e8b5f9f1a8e563d7d35a48c0386401f Mon Sep 17 00:00:00 2001 From: MaelkMark <110915409+MaelkMark@users.noreply.github.com> Date: Thu, 3 Apr 2025 17:33:06 +0200 Subject: [PATCH 141/141] Updated WebSocketHandler ssl to work with Python 3.12+ Replaced ssl.wrap_socket() with the new ssl.SSLContext approach in WebSocketHandler __init__ as Python 3.12+ no longer supports the ssl.wrap_socket() function. --- websocket_server/websocket_server.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/websocket_server/websocket_server.py b/websocket_server/websocket_server.py index 083ee17..c954c34 100644 --- a/websocket_server/websocket_server.py +++ b/websocket_server/websocket_server.py @@ -265,9 +265,13 @@ def __init__(self, socket, addr, server): self._send_lock = threading.Lock() if server.key and server.cert: try: - socket = ssl.wrap_socket(socket, server_side=True, certfile=server.cert, keyfile=server.key) - except: # Not sure which exception it throws if the key/cert isn't found - logger.warning("SSL not available (are the paths {} and {} correct for the key and cert?)".format(server.key, server.cert)) + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.load_cert_chain(certfile=server.cert, keyfile=server.key) + socket = ssl_context.wrap_socket(socket, server_side=True) + except FileNotFoundError: + logger.warning("SSL key or certificate file not found. Please check the paths for the key and cert.") + except ssl.SSLError as e: + logger.warning(f"SSL error occurred: {e}") StreamRequestHandler.__init__(self, socket, addr, server) def setup(self):