From 6a64314f7e54fa89df8502e5211d96e56576940a Mon Sep 17 00:00:00 2001 From: Mano Date: Mon, 15 Nov 2021 10:08:11 +0000 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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 05/13] 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 06/13] 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 07/13] 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 08/13] 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 09/13] 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 10/13] 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 11/13] 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 12/13] 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 13/13] 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):