From 46d2ed4d402e2076583e6b8686eb2ea9e284847e Mon Sep 17 00:00:00 2001 From: Pithikos Date: Thu, 16 Mar 2017 11:25:57 +0000 Subject: [PATCH 001/111] 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 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 002/111] 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 003/111] 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 004/111] 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 005/111] 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 006/111] 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 007/111] 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 008/111] 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 009/111] 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 010/111] 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 011/111] 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 012/111] 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 013/111] 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 014/111] 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 015/111] 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 016/111] 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 017/111] 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 018/111] 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 019/111] 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 020/111] 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 021/111] 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 022/111] 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 023/111] 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 024/111] 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 025/111] 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 026/111] 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 027/111] 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 028/111] 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 029/111] 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 030/111] 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 031/111] 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 032/111] 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 033/111] 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 034/111] 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 035/111] 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 036/111] 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 037/111] 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 038/111] 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 039/111] 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 040/111] 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 041/111] 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 042/111] 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 043/111] 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 044/111] 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 045/111] 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 046/111] 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 047/111] 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 048/111] 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 049/111] 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 050/111] 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 051/111] 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 052/111] 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 053/111] 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 054/111] 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 055/111] 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 056/111] 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 057/111] 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 058/111] 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 059/111] 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 060/111] 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 061/111] 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 062/111] 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 063/111] 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 064/111] 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 065/111] 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 066/111] 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 067/111] 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 068/111] 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 069/111] 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 070/111] 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 071/111] 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 072/111] 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 073/111] 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 074/111] 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 075/111] 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 076/111] 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 077/111] 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 078/111] 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 079/111] 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 080/111] 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 081/111] 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 082/111] 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 083/111] 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 084/111] 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 085/111] 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 086/111] 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 087/111] 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 088/111] 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 089/111] 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 090/111] 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 091/111] 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 092/111] 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 093/111] 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 094/111] 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 095/111] 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 096/111] 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 097/111] 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 098/111] 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 099/111] 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 100/111] 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 101/111] 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 102/111] 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 103/111] 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 104/111] 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 105/111] 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 106/111] 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 107/111] 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 108/111] 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 109/111] 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 110/111] 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 111/111] 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):