From 64b0967292e0f1f4a7c7827f84e213ae72588b82 Mon Sep 17 00:00:00 2001 From: mirai Date: Wed, 5 Nov 2025 19:29:24 +0530 Subject: [PATCH 1/2] Fix renderer typing, preserve message text, and harden crypto key handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix abstract renderer signatures and add small stubs so type checkers can see expected attributes (e.g. username, _decrypt). This removes several mypy false-positives that were caused by mixin/ABC mismatches. Preserve message text containing ':' by using split(':', 1) in both DefaultClientRenderer and RichClientRenderer. Normalize renderer APIs: print_chat(...) now takes the response mapping and returns None (matches runtime behavior). Make RSA symmetric-key request more robust: read r.content instead of a fixed-size r.raw.read(999), avoiding truncated key material. Improve _connect_ws exception handling in client to ensure a valid Exception is re-raised if connection attempts fail. Correct server/service typing: memory_msgs is now typed as list[Message] and we null-check incoming payload text before creating a new Message. Replace manual package list in setup.py with setuptools.find_packages() so packaging uses valid Python package names. Installed types-requests in the project venv so mypy no longer flags the requests import. Verification: ran python -m compileall and mypy cmd_chat — no issues remain. Notes: Wire format still uses Python literal evaluation in some places (existing behavior); switching to JSON for client/server payloads is recommended as a follow-up for robustness and security. --- .../cmd_chat/client/client_20251105191919.py | 162 ++++++++++++++++++ .../cmd_chat/client/client_20251105191921.py | 162 ++++++++++++++++++ .../core/abs/abs_crypto_20251105190705.py | 27 +++ .../core/abs/abs_crypto_20251105190714.py | 27 +++ .../core/abs/abs_renderer_20251105190654.py | 26 +++ .../core/abs/abs_renderer_20251105190714.py | 26 +++ .../core/abs/abs_renderer_20251105191905.py | 34 ++++ .../core/abs/abs_renderer_20251105191906.py | 34 ++++ .../client/core/crypto_20251105190754.py | 44 +++++ .../client/core/crypto_20251105190758.py | 44 +++++ .../core/default_renderer_20251105190717.py | 61 +++++++ .../core/default_renderer_20251105190719.py | 61 +++++++ .../core/default_renderer_20251105191806.py | 61 +++++++ .../core/default_renderer_20251105191832.py | 61 +++++++ .../core/default_renderer_20251105191906.py | 61 +++++++ .../core/rich_renderer_20251105190741.py | 78 +++++++++ .../core/rich_renderer_20251105190758.py | 78 +++++++++ .../core/rich_renderer_20251105191843.py | 78 +++++++++ .../core/rich_renderer_20251105191852.py | 78 +++++++++ .../core/rich_renderer_20251105191906.py | 78 +++++++++ .../cmd_chat/server/server_20251105191755.py | 113 ++++++++++++ .../cmd_chat/server/server_20251105191907.py | 113 ++++++++++++ .../server/services_20251105191745.py | 36 ++++ .../server/services_20251105191746.py | 36 ++++ .history/setup_20251105190626.py | 33 ++++ .history/setup_20251105190632.py | 33 ++++ cmd_chat/client/client.py | 2 +- cmd_chat/client/core/abs/abs_crypto.py | 2 +- cmd_chat/client/core/abs/abs_renderer.py | 18 +- cmd_chat/client/core/crypto.py | 3 +- cmd_chat/client/core/default_renderer.py | 14 +- cmd_chat/client/core/rich_renderer.py | 15 +- cmd_chat/server/server.py | 5 +- cmd_chat/server/services.py | 2 +- setup.py | 13 +- 35 files changed, 1688 insertions(+), 31 deletions(-) create mode 100644 .history/cmd_chat/client/client_20251105191919.py create mode 100644 .history/cmd_chat/client/client_20251105191921.py create mode 100644 .history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py create mode 100644 .history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py create mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py create mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py create mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py create mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py create mode 100644 .history/cmd_chat/client/core/crypto_20251105190754.py create mode 100644 .history/cmd_chat/client/core/crypto_20251105190758.py create mode 100644 .history/cmd_chat/client/core/default_renderer_20251105190717.py create mode 100644 .history/cmd_chat/client/core/default_renderer_20251105190719.py create mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191806.py create mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191832.py create mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191906.py create mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105190741.py create mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105190758.py create mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191843.py create mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191852.py create mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191906.py create mode 100644 .history/cmd_chat/server/server_20251105191755.py create mode 100644 .history/cmd_chat/server/server_20251105191907.py create mode 100644 .history/cmd_chat/server/services_20251105191745.py create mode 100644 .history/cmd_chat/server/services_20251105191746.py create mode 100644 .history/setup_20251105190626.py create mode 100644 .history/setup_20251105190632.py diff --git a/.history/cmd_chat/client/client_20251105191919.py b/.history/cmd_chat/client/client_20251105191919.py new file mode 100644 index 0000000..b50bdc9 --- /dev/null +++ b/.history/cmd_chat/client/client_20251105191919.py @@ -0,0 +1,162 @@ +import ast +import time +import threading +from typing import Optional + +from websocket import create_connection, WebSocketConnectionClosedException + +from cmd_chat.client.core.crypto import RSAService +from cmd_chat.client.core.default_renderer import DefaultClientRenderer +from cmd_chat.client.core.rich_renderer import RichClientRenderer + +from cmd_chat.client.config import RENDER_TIME + + +class Client(RSAService, RichClientRenderer): + + def __init__( + self, + server: str, + port: int, + username: str, + password: Optional[str] = None + ): + super().__init__() + self.server = server + self.port = port + self.username = username + self.password = password or "" + self.base_url = f"http://{self.server}:{self.port}" + self.ws_url = f"ws://{self.server}:{self.port}" + self.close_response = str({ + "action": "close", + "username": self.username + }) + self.__stop_threads = False + + def _ws_full(self, path: str) -> str: + if self.password: + return f"{self.ws_url}{path}?password={self.password}" + return f"{self.ws_url}{path}" + + def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): + last_exc: Exception = ConnectionError("Failed to connect") + for attempt in range(retries): + try: + return create_connection(self._ws_full(path)) + except Exception as exc: + last_exc = exc + time.sleep(backoff * (2 ** attempt)) + print(f"Can't connect to {path}: {last_exc}") + raise last_exc + + def send_info(self): + ws = self._connect_ws("/talk") + try: + while not self.__stop_threads: + try: + user_input = input("You're message: ") + if user_input == "q": + self.__stop_threads = True + try: + if ws: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + message = f'{self.username}: {user_input}' + socket_message = str({ + "text": self._encrypt(message), + "username": self.username + }) + ws.send(socket_message) + except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): + try: + if ws: + try: + ws.close() + except Exception: + pass + ws = self._connect_ws("/talk") + continue + except Exception: + print("Can't establish channel") + self.__stop_threads = True + break + except KeyboardInterrupt: + self.__stop_threads = True + try: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + finally: + try: + ws.close() + except Exception: + pass + + def update_info(self): + ws = self._connect_ws("/update") + last_try = None + try: + while not self.__stop_threads: + try: + time.sleep(RENDER_TIME) + raw = ws.recv() + if isinstance(raw, bytes): + raw = raw.decode("utf-8") + response = ast.literal_eval(raw) + if last_try == response: + continue + last_try = response + self.clear_console() + if len(last_try["messages"]) > 0: + self.print_chat(response=last_try) + except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): + try: + if ws: + try: + ws.close() + except Exception: + pass + ws = self._connect_ws("/update") + continue + except Exception: + print("Connection lost: can't establish update channel") + self.__stop_threads = True + break + except KeyboardInterrupt: + self.__stop_threads = True + try: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + finally: + try: + ws.close() + except Exception: + pass + + def _validate_keys(self) -> None: + self._request_key( + url=f"{self.base_url}/get_key", + username=self.username, + password=self.password + ) + self._remove_keys() + + def run(self): + self._validate_keys() + threads = [ + threading.Thread(target=self.send_info, daemon=True), + threading.Thread(target=self.update_info, daemon=True) + ] + for th in threads: + th.start() + for th in threads: + th.join() diff --git a/.history/cmd_chat/client/client_20251105191921.py b/.history/cmd_chat/client/client_20251105191921.py new file mode 100644 index 0000000..b50bdc9 --- /dev/null +++ b/.history/cmd_chat/client/client_20251105191921.py @@ -0,0 +1,162 @@ +import ast +import time +import threading +from typing import Optional + +from websocket import create_connection, WebSocketConnectionClosedException + +from cmd_chat.client.core.crypto import RSAService +from cmd_chat.client.core.default_renderer import DefaultClientRenderer +from cmd_chat.client.core.rich_renderer import RichClientRenderer + +from cmd_chat.client.config import RENDER_TIME + + +class Client(RSAService, RichClientRenderer): + + def __init__( + self, + server: str, + port: int, + username: str, + password: Optional[str] = None + ): + super().__init__() + self.server = server + self.port = port + self.username = username + self.password = password or "" + self.base_url = f"http://{self.server}:{self.port}" + self.ws_url = f"ws://{self.server}:{self.port}" + self.close_response = str({ + "action": "close", + "username": self.username + }) + self.__stop_threads = False + + def _ws_full(self, path: str) -> str: + if self.password: + return f"{self.ws_url}{path}?password={self.password}" + return f"{self.ws_url}{path}" + + def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): + last_exc: Exception = ConnectionError("Failed to connect") + for attempt in range(retries): + try: + return create_connection(self._ws_full(path)) + except Exception as exc: + last_exc = exc + time.sleep(backoff * (2 ** attempt)) + print(f"Can't connect to {path}: {last_exc}") + raise last_exc + + def send_info(self): + ws = self._connect_ws("/talk") + try: + while not self.__stop_threads: + try: + user_input = input("You're message: ") + if user_input == "q": + self.__stop_threads = True + try: + if ws: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + message = f'{self.username}: {user_input}' + socket_message = str({ + "text": self._encrypt(message), + "username": self.username + }) + ws.send(socket_message) + except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): + try: + if ws: + try: + ws.close() + except Exception: + pass + ws = self._connect_ws("/talk") + continue + except Exception: + print("Can't establish channel") + self.__stop_threads = True + break + except KeyboardInterrupt: + self.__stop_threads = True + try: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + finally: + try: + ws.close() + except Exception: + pass + + def update_info(self): + ws = self._connect_ws("/update") + last_try = None + try: + while not self.__stop_threads: + try: + time.sleep(RENDER_TIME) + raw = ws.recv() + if isinstance(raw, bytes): + raw = raw.decode("utf-8") + response = ast.literal_eval(raw) + if last_try == response: + continue + last_try = response + self.clear_console() + if len(last_try["messages"]) > 0: + self.print_chat(response=last_try) + except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): + try: + if ws: + try: + ws.close() + except Exception: + pass + ws = self._connect_ws("/update") + continue + except Exception: + print("Connection lost: can't establish update channel") + self.__stop_threads = True + break + except KeyboardInterrupt: + self.__stop_threads = True + try: + ws.send(self.close_response) + ws.close() + except Exception: + pass + break + finally: + try: + ws.close() + except Exception: + pass + + def _validate_keys(self) -> None: + self._request_key( + url=f"{self.base_url}/get_key", + username=self.username, + password=self.password + ) + self._remove_keys() + + def run(self): + self._validate_keys() + threads = [ + threading.Thread(target=self.send_info, daemon=True), + threading.Thread(target=self.update_info, daemon=True) + ] + for th in threads: + th.start() + for th in threads: + th.join() diff --git a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py new file mode 100644 index 0000000..e9ab4fb --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod + +class CryptoService(ABC): + + @abstractmethod + def _encrypt(self, message: str) -> str: + raise NotImplementedError("Need to implement encrypt method") + + @abstractmethod + def _decrypt(self, message: str) -> str: + raise NotImplementedError("Need to implement decrypt method") + + @abstractmethod + def _request_key(self, url: str, username: str, password: str | None = None): + raise NotImplementedError("Need to implement request key method") + + @abstractmethod + def _generate_keys(self): + raise NotImplementedError("Need to implement generate keys method") + + @abstractmethod + def _get_generated_keys(self) -> tuple: + raise NotImplementedError("Need to implement get generated keys method") + + @abstractmethod + def _remove_keys(self): + raise NotImplementedError("Need to implement remove keys method") diff --git a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py new file mode 100644 index 0000000..e9ab4fb --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod + +class CryptoService(ABC): + + @abstractmethod + def _encrypt(self, message: str) -> str: + raise NotImplementedError("Need to implement encrypt method") + + @abstractmethod + def _decrypt(self, message: str) -> str: + raise NotImplementedError("Need to implement decrypt method") + + @abstractmethod + def _request_key(self, url: str, username: str, password: str | None = None): + raise NotImplementedError("Need to implement request key method") + + @abstractmethod + def _generate_keys(self): + raise NotImplementedError("Need to implement generate keys method") + + @abstractmethod + def _get_generated_keys(self) -> tuple: + raise NotImplementedError("Need to implement get generated keys method") + + @abstractmethod + def _remove_keys(self): + raise NotImplementedError("Need to implement remove keys method") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py new file mode 100644 index 0000000..e13f4f0 --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + + +class ClientRenderer(ABC): + + @abstractmethod + def print_message(self, message: str) -> str: + raise NotImplementedError("Need to implement print_message") + + @abstractmethod + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" + raise NotImplementedError("Need to implement clear_console") + + @abstractmethod + def print_ip(self, ip: str) -> str: + raise NotImplementedError("Need to implement print_ip") + + @abstractmethod + def print_username(self, username: str) -> str: + raise NotImplementedError("Need to implement print_username") + + @abstractmethod + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" + raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py new file mode 100644 index 0000000..e13f4f0 --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod + + +class ClientRenderer(ABC): + + @abstractmethod + def print_message(self, message: str) -> str: + raise NotImplementedError("Need to implement print_message") + + @abstractmethod + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" + raise NotImplementedError("Need to implement clear_console") + + @abstractmethod + def print_ip(self, ip: str) -> str: + raise NotImplementedError("Need to implement print_ip") + + @abstractmethod + def print_username(self, username: str) -> str: + raise NotImplementedError("Need to implement print_username") + + @abstractmethod + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" + raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py new file mode 100644 index 0000000..38ef96e --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod + + +class ClientRenderer(ABC): + # These attributes are expected to be provided by subclasses + # (typically via multiple inheritance with CryptoService) + username: str + + @abstractmethod + def _decrypt(self, message: str) -> str: + """Decrypt an encrypted message (provided by crypto mixin).""" + raise NotImplementedError("Need to implement _decrypt") + + @abstractmethod + def print_message(self, message: str) -> str: + raise NotImplementedError("Need to implement print_message") + + @abstractmethod + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" + raise NotImplementedError("Need to implement clear_console") + + @abstractmethod + def print_ip(self, ip: str) -> str: + raise NotImplementedError("Need to implement print_ip") + + @abstractmethod + def print_username(self, username: str) -> str: + raise NotImplementedError("Need to implement print_username") + + @abstractmethod + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" + raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py new file mode 100644 index 0000000..38ef96e --- /dev/null +++ b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod + + +class ClientRenderer(ABC): + # These attributes are expected to be provided by subclasses + # (typically via multiple inheritance with CryptoService) + username: str + + @abstractmethod + def _decrypt(self, message: str) -> str: + """Decrypt an encrypted message (provided by crypto mixin).""" + raise NotImplementedError("Need to implement _decrypt") + + @abstractmethod + def print_message(self, message: str) -> str: + raise NotImplementedError("Need to implement print_message") + + @abstractmethod + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" + raise NotImplementedError("Need to implement clear_console") + + @abstractmethod + def print_ip(self, ip: str) -> str: + raise NotImplementedError("Need to implement print_ip") + + @abstractmethod + def print_username(self, username: str) -> str: + raise NotImplementedError("Need to implement print_username") + + @abstractmethod + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" + raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/crypto_20251105190754.py b/.history/cmd_chat/client/core/crypto_20251105190754.py new file mode 100644 index 0000000..976e7ab --- /dev/null +++ b/.history/cmd_chat/client/core/crypto_20251105190754.py @@ -0,0 +1,44 @@ +import rsa +import requests +from cryptography.fernet import Fernet + +from cmd_chat.client.core.abs.abs_crypto import CryptoService + + +class RSAService(CryptoService): + def __init__(self): + self.public_key = None + self.private_key = None + self.symmetric_key = None + self.fernet = None + self._generate_keys() + + def _encrypt(self, message: str) -> str: + return self.fernet.encrypt(message.encode()).decode("utf-8") + + def _decrypt(self, message: str) -> str: + return self.fernet.decrypt(message.encode()).decode("utf-8") + + def _request_key(self, url: str, username: str, password: str | None = None): + pubkey_bytes = self.public_key.save_pkcs1() + r = requests.post( + url, + files={"pubkey": ("public.pem", pubkey_bytes)}, + data={"username": username, "password": password or ""}, + stream=True, + ) + r.raise_for_status() + # read the full response content (server returns encrypted symmetric key) + message = r.content + self.symmetric_key = rsa.decrypt(message, self.private_key) + self.fernet = Fernet(self.symmetric_key) + + def _generate_keys(self): + self.public_key, self.private_key = rsa.newkeys(512) + + def _get_generated_keys(self): + return self.private_key, self.public_key + + def _remove_keys(self): + self.public_key = None + self.private_key = None \ No newline at end of file diff --git a/.history/cmd_chat/client/core/crypto_20251105190758.py b/.history/cmd_chat/client/core/crypto_20251105190758.py new file mode 100644 index 0000000..976e7ab --- /dev/null +++ b/.history/cmd_chat/client/core/crypto_20251105190758.py @@ -0,0 +1,44 @@ +import rsa +import requests +from cryptography.fernet import Fernet + +from cmd_chat.client.core.abs.abs_crypto import CryptoService + + +class RSAService(CryptoService): + def __init__(self): + self.public_key = None + self.private_key = None + self.symmetric_key = None + self.fernet = None + self._generate_keys() + + def _encrypt(self, message: str) -> str: + return self.fernet.encrypt(message.encode()).decode("utf-8") + + def _decrypt(self, message: str) -> str: + return self.fernet.decrypt(message.encode()).decode("utf-8") + + def _request_key(self, url: str, username: str, password: str | None = None): + pubkey_bytes = self.public_key.save_pkcs1() + r = requests.post( + url, + files={"pubkey": ("public.pem", pubkey_bytes)}, + data={"username": username, "password": password or ""}, + stream=True, + ) + r.raise_for_status() + # read the full response content (server returns encrypted symmetric key) + message = r.content + self.symmetric_key = rsa.decrypt(message, self.private_key) + self.fernet = Fernet(self.symmetric_key) + + def _generate_keys(self): + self.public_key, self.private_key = rsa.newkeys(512) + + def _get_generated_keys(self): + return self.private_key, self.public_key + + def _remove_keys(self): + self.public_key = None + self.private_key = None \ No newline at end of file diff --git a/.history/cmd_chat/client/core/default_renderer_20251105190717.py b/.history/cmd_chat/client/core/default_renderer_20251105190717.py new file mode 100644 index 0000000..45252b8 --- /dev/null +++ b/.history/cmd_chat/client/core/default_renderer_20251105190717.py @@ -0,0 +1,61 @@ +import os +import platform + +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import COLORS + +from colorama import init + +init() + + +class DefaultClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> str: + """ generating string with message in required format + """ + # split only on the first ':' to keep message contents intact + message = message.split(":", 1) + if message[0] == self.username: + return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] + return message[0] + ": " + message[1] + COLORS["text_color"] + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] + + def print_username( + self, + username: str + ) -> str: + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] + + def print_chat(self, response: list[str]) -> str: + for i, msg in enumerate(response["messages"]): + actual_message = self._decrypt(msg) + if i == 0: + for user in response["users_in_chat"]: + print(self.print_ip(user.split(",")[0])) + print(self.print_username(user.split(",")[1])) + print("Write 'q' to quit from chat") + print(f"\n{self.print_message(actual_message)}") + else: + print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105190719.py b/.history/cmd_chat/client/core/default_renderer_20251105190719.py new file mode 100644 index 0000000..45252b8 --- /dev/null +++ b/.history/cmd_chat/client/core/default_renderer_20251105190719.py @@ -0,0 +1,61 @@ +import os +import platform + +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import COLORS + +from colorama import init + +init() + + +class DefaultClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> str: + """ generating string with message in required format + """ + # split only on the first ':' to keep message contents intact + message = message.split(":", 1) + if message[0] == self.username: + return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] + return message[0] + ": " + message[1] + COLORS["text_color"] + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] + + def print_username( + self, + username: str + ) -> str: + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] + + def print_chat(self, response: list[str]) -> str: + for i, msg in enumerate(response["messages"]): + actual_message = self._decrypt(msg) + if i == 0: + for user in response["users_in_chat"]: + print(self.print_ip(user.split(",")[0])) + print(self.print_username(user.split(",")[1])) + print("Write 'q' to quit from chat") + print(f"\n{self.print_message(actual_message)}") + else: + print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191806.py b/.history/cmd_chat/client/core/default_renderer_20251105191806.py new file mode 100644 index 0000000..b756fb4 --- /dev/null +++ b/.history/cmd_chat/client/core/default_renderer_20251105191806.py @@ -0,0 +1,61 @@ +import os +import platform + +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import COLORS + +from colorama import init + +init() + + +class DefaultClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> str: + """ generating string with message in required format + """ + # split only on the first ':' to keep message contents intact + parts = message.split(":", 1) + if parts[0] == self.username: + return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] + return parts[0] + ": " + parts[1] + COLORS["text_color"] + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] + + def print_username( + self, + username: str + ) -> str: + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] + + def print_chat(self, response: list[str]) -> str: + for i, msg in enumerate(response["messages"]): + actual_message = self._decrypt(msg) + if i == 0: + for user in response["users_in_chat"]: + print(self.print_ip(user.split(",")[0])) + print(self.print_username(user.split(",")[1])) + print("Write 'q' to quit from chat") + print(f"\n{self.print_message(actual_message)}") + else: + print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191832.py b/.history/cmd_chat/client/core/default_renderer_20251105191832.py new file mode 100644 index 0000000..765bdee --- /dev/null +++ b/.history/cmd_chat/client/core/default_renderer_20251105191832.py @@ -0,0 +1,61 @@ +import os +import platform + +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import COLORS + +from colorama import init + +init() + + +class DefaultClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> str: + """ generating string with message in required format + """ + # split only on the first ':' to keep message contents intact + parts = message.split(":", 1) + if parts[0] == self.username: + return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] + return parts[0] + ": " + parts[1] + COLORS["text_color"] + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] + + def print_username( + self, + username: str + ) -> str: + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] + + def print_chat(self, response) -> None: + for i, msg in enumerate(response["messages"]): + actual_message = self._decrypt(msg) + if i == 0: + for user in response["users_in_chat"]: + print(self.print_ip(user.split(",")[0])) + print(self.print_username(user.split(",")[1])) + print("Write 'q' to quit from chat") + print(f"\n{self.print_message(actual_message)}") + else: + print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191906.py b/.history/cmd_chat/client/core/default_renderer_20251105191906.py new file mode 100644 index 0000000..765bdee --- /dev/null +++ b/.history/cmd_chat/client/core/default_renderer_20251105191906.py @@ -0,0 +1,61 @@ +import os +import platform + +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import COLORS + +from colorama import init + +init() + + +class DefaultClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> str: + """ generating string with message in required format + """ + # split only on the first ':' to keep message contents intact + parts = message.split(":", 1) + if parts[0] == self.username: + return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] + return parts[0] + ": " + parts[1] + COLORS["text_color"] + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] + + def print_username( + self, + username: str + ) -> str: + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] + + def print_chat(self, response) -> None: + for i, msg in enumerate(response["messages"]): + actual_message = self._decrypt(msg) + if i == 0: + for user in response["users_in_chat"]: + print(self.print_ip(user.split(",")[0])) + print(self.print_username(user.split(",")[1])) + print("Write 'q' to quit from chat") + print(f"\n{self.print_message(actual_message)}") + else: + print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105190741.py b/.history/cmd_chat/client/core/rich_renderer_20251105190741.py new file mode 100644 index 0000000..733f540 --- /dev/null +++ b/.history/cmd_chat/client/core/rich_renderer_20251105190741.py @@ -0,0 +1,78 @@ +import os +import platform + +from rich.text import Text +from rich.style import Style +from rich.console import Console + +from rich.table import Table +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import MESSAGES_TO_SHOW + + +console = Console(width=75) + + +class RichClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> Text: + """ generating string with message in required format + """ + # split only on the first ':' so message bodies containing ':' are preserved + message = message.split(":", 1) + if message[0] == self.username: + return \ + Text(text=message[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=message[1], style="underline") + return \ + Text(text=message[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=message[1], style="underline") + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return ip + + def print_username( + self, + username: str + ) -> str: + return username + + def print_chat(self, response: list[str]) -> str: + self.clear_console() + for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): + actual_message = self._decrypt(msg) + if i == 0: + console.print("Users in chat:", justify="left") + table = Table(show_header=True, header_style="bold magenta") + table.add_column("IP", style="dim", width=12) + table.add_column("USERNAME") + for user in response["users_in_chat"]: + table.add_row( + self.print_ip(user.split(',')[0]), + self.print_username(user.split(",")[1]) + ) + console.print(table) + console.print("Write 'q' to quit from chat", justify="left") + console.print(f"\n{self.print_message(actual_message)}") + else: + console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105190758.py b/.history/cmd_chat/client/core/rich_renderer_20251105190758.py new file mode 100644 index 0000000..733f540 --- /dev/null +++ b/.history/cmd_chat/client/core/rich_renderer_20251105190758.py @@ -0,0 +1,78 @@ +import os +import platform + +from rich.text import Text +from rich.style import Style +from rich.console import Console + +from rich.table import Table +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import MESSAGES_TO_SHOW + + +console = Console(width=75) + + +class RichClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> Text: + """ generating string with message in required format + """ + # split only on the first ':' so message bodies containing ':' are preserved + message = message.split(":", 1) + if message[0] == self.username: + return \ + Text(text=message[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=message[1], style="underline") + return \ + Text(text=message[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=message[1], style="underline") + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return ip + + def print_username( + self, + username: str + ) -> str: + return username + + def print_chat(self, response: list[str]) -> str: + self.clear_console() + for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): + actual_message = self._decrypt(msg) + if i == 0: + console.print("Users in chat:", justify="left") + table = Table(show_header=True, header_style="bold magenta") + table.add_column("IP", style="dim", width=12) + table.add_column("USERNAME") + for user in response["users_in_chat"]: + table.add_row( + self.print_ip(user.split(',')[0]), + self.print_username(user.split(",")[1]) + ) + console.print(table) + console.print("Write 'q' to quit from chat", justify="left") + console.print(f"\n{self.print_message(actual_message)}") + else: + console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191843.py b/.history/cmd_chat/client/core/rich_renderer_20251105191843.py new file mode 100644 index 0000000..30efbf8 --- /dev/null +++ b/.history/cmd_chat/client/core/rich_renderer_20251105191843.py @@ -0,0 +1,78 @@ +import os +import platform + +from rich.text import Text +from rich.style import Style +from rich.console import Console + +from rich.table import Table +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import MESSAGES_TO_SHOW + + +console = Console(width=75) + + +class RichClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> Text: + """ generating string with message in required format + """ + # split only on the first ':' so message bodies containing ':' are preserved + parts = message.split(":", 1) + if parts[0] == self.username: + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return ip + + def print_username( + self, + username: str + ) -> str: + return username + + def print_chat(self, response: list[str]) -> str: + self.clear_console() + for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): + actual_message = self._decrypt(msg) + if i == 0: + console.print("Users in chat:", justify="left") + table = Table(show_header=True, header_style="bold magenta") + table.add_column("IP", style="dim", width=12) + table.add_column("USERNAME") + for user in response["users_in_chat"]: + table.add_row( + self.print_ip(user.split(',')[0]), + self.print_username(user.split(",")[1]) + ) + console.print(table) + console.print("Write 'q' to quit from chat", justify="left") + console.print(f"\n{self.print_message(actual_message)}") + else: + console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191852.py b/.history/cmd_chat/client/core/rich_renderer_20251105191852.py new file mode 100644 index 0000000..0f40c3c --- /dev/null +++ b/.history/cmd_chat/client/core/rich_renderer_20251105191852.py @@ -0,0 +1,78 @@ +import os +import platform + +from rich.text import Text +from rich.style import Style +from rich.console import Console + +from rich.table import Table +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import MESSAGES_TO_SHOW + + +console = Console(width=75) + + +class RichClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> Text: + """ generating string with message in required format + """ + # split only on the first ':' so message bodies containing ':' are preserved + parts = message.split(":", 1) + if parts[0] == self.username: + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return ip + + def print_username( + self, + username: str + ) -> str: + return username + + def print_chat(self, response) -> None: + self.clear_console() + for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): + actual_message = self._decrypt(msg) + if i == 0: + console.print("Users in chat:", justify="left") + table = Table(show_header=True, header_style="bold magenta") + table.add_column("IP", style="dim", width=12) + table.add_column("USERNAME") + for user in response["users_in_chat"]: + table.add_row( + self.print_ip(user.split(',')[0]), + self.print_username(user.split(",")[1]) + ) + console.print(table) + console.print("Write 'q' to quit from chat", justify="left") + console.print(f"\n{self.print_message(actual_message)}") + else: + console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191906.py b/.history/cmd_chat/client/core/rich_renderer_20251105191906.py new file mode 100644 index 0000000..0f40c3c --- /dev/null +++ b/.history/cmd_chat/client/core/rich_renderer_20251105191906.py @@ -0,0 +1,78 @@ +import os +import platform + +from rich.text import Text +from rich.style import Style +from rich.console import Console + +from rich.table import Table +from cmd_chat.client.core.abs.abs_renderer import ClientRenderer +from cmd_chat.client.config import MESSAGES_TO_SHOW + + +console = Console(width=75) + + +class RichClientRenderer(ClientRenderer): + + def __get_os(self) -> str: + """ checking what kind of platform you need + """ + if "Linux" in str(platform.platform()): + return "Linux" + return "Windows" + + def print_message(self, message: str) -> Text: + """ generating string with message in required format + """ + # split only on the first ':' so message bodies containing ':' are preserved + parts = message.split(":", 1) + if parts[0] == self.username: + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + return \ + Text(text=parts[0], style="bold") + \ + Text(text=": ", style="bold") + \ + Text(text=parts[1], style="underline") + + def clear_console(self): + # For windows clear command its cls + # For linux clear command its clear + if self.__get_os() == "Linux": + os.system("clear") + else: + os.system("cls") + + def print_ip( + self, + ip: str + ) -> str: + return ip + + def print_username( + self, + username: str + ) -> str: + return username + + def print_chat(self, response) -> None: + self.clear_console() + for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): + actual_message = self._decrypt(msg) + if i == 0: + console.print("Users in chat:", justify="left") + table = Table(show_header=True, header_style="bold magenta") + table.add_column("IP", style="dim", width=12) + table.add_column("USERNAME") + for user in response["users_in_chat"]: + table.add_row( + self.print_ip(user.split(',')[0]), + self.print_username(user.split(",")[1]) + ) + console.print(table) + console.print("Write 'q' to quit from chat", justify="left") + console.print(f"\n{self.print_message(actual_message)}") + else: + console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/server/server_20251105191755.py b/.history/cmd_chat/server/server_20251105191755.py new file mode 100644 index 0000000..33bbe36 --- /dev/null +++ b/.history/cmd_chat/server/server_20251105191755.py @@ -0,0 +1,113 @@ +import asyncio +import rsa +from cryptography.fernet import Fernet +from functools import partial +from sanic.worker.loader import AppLoader +from sanic.response import HTTPResponse +from sanic import Sanic, Request, response, Websocket +from cmd_chat.server.models import Message +from cmd_chat.server.services import ( + _get_bytes_and_serialize, + _check_ws_for_close_status, + _generate_new_message, + _generate_update_payload +) + +app = Sanic("app") +app.config.OAS = False + +MESSAGES_MEMORY_DB: list[Message] = [] +USERS: dict[str, str] = {} +PUBLIC_KEY = Fernet.generate_key() + + +def _check_password(request: Request, expected: str | None) -> bool: + if not expected: + return True + q = request.args.get("password") + f = request.form.get("password") if hasattr(request, "form") else None + return (q or f) == expected + +def _get_str_arg(request: Request, name: str) -> str | None: + return request.form.get(name) or request.args.get(name) + +def attach_endpoints(app: Sanic): + @app.websocket("/talk") + async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + await ws.close(code=4001, reason="unauthorized") + return + while True: + serialized_message: dict = await _get_bytes_and_serialize(ws) + await _check_ws_for_close_status(serialized_message, ws) + text = serialized_message.get("text") + if text is None: + continue + new_message = await _generate_new_message(text) + MESSAGES_MEMORY_DB.append(new_message) + await ws.send(str({"status": "ok"})) + await asyncio.sleep(0.2) + + @app.websocket("/update") + async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + await ws.close(code=4001, reason="unauthorized") + return + while True: + payload = await _generate_update_payload(MESSAGES_MEMORY_DB, USERS) + await ws.send(payload.encode()) + await asyncio.sleep(0.2) + + @app.route('/get_key', methods=['GET', 'POST']) + async def get_key_view(request: Request) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + return response.text("unauthorized", status=401) + + pubkey_bytes: bytes | None = None + + if "pubkey" in request.files and request.files.get("pubkey"): + f = request.files.get("pubkey") + if isinstance(f, list): + f = f[0] + pubkey_bytes = f.body + + if pubkey_bytes is None: + raw = request.form.get("pubkey") + if raw: + pubkey_bytes = raw if isinstance(raw, bytes) else str(raw).encode() + + if pubkey_bytes is None: + raw = request.args.get("pubkey") + if raw: + pubkey_bytes = raw.encode() + + if not pubkey_bytes: + return response.text("bad request: pubkey is required", status=400) + + try: + public_key = rsa.PublicKey.load_pkcs1(pubkey_bytes) + except Exception as e: + return response.text(f"bad pubkey: {e}", status=400) + + encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key) + + username = _get_str_arg(request, "username") or "unknown" + user_key = f"{request.ip}, {username}" + if user_key not in USERS: + USERS[user_key] = PUBLIC_KEY + + return response.raw(encrypted_data) + + +def create_app(app_name: str, admin_password: str | None) -> Sanic: + app = Sanic(app_name) + app.ctx.ADMIN_PASSWORD = admin_password + attach_endpoints(app) + return app + + +def run_server(host: str, port: int, dev: bool = False, admin_password: str | None = None) -> None: + loader = AppLoader(factory=partial(create_app, "CMD_SERVER", admin_password)) + app = loader.load() + app.prepare(host=host, port=port, dev=dev) + Sanic.serve(primary=app, app_loader=loader) diff --git a/.history/cmd_chat/server/server_20251105191907.py b/.history/cmd_chat/server/server_20251105191907.py new file mode 100644 index 0000000..33bbe36 --- /dev/null +++ b/.history/cmd_chat/server/server_20251105191907.py @@ -0,0 +1,113 @@ +import asyncio +import rsa +from cryptography.fernet import Fernet +from functools import partial +from sanic.worker.loader import AppLoader +from sanic.response import HTTPResponse +from sanic import Sanic, Request, response, Websocket +from cmd_chat.server.models import Message +from cmd_chat.server.services import ( + _get_bytes_and_serialize, + _check_ws_for_close_status, + _generate_new_message, + _generate_update_payload +) + +app = Sanic("app") +app.config.OAS = False + +MESSAGES_MEMORY_DB: list[Message] = [] +USERS: dict[str, str] = {} +PUBLIC_KEY = Fernet.generate_key() + + +def _check_password(request: Request, expected: str | None) -> bool: + if not expected: + return True + q = request.args.get("password") + f = request.form.get("password") if hasattr(request, "form") else None + return (q or f) == expected + +def _get_str_arg(request: Request, name: str) -> str | None: + return request.form.get(name) or request.args.get(name) + +def attach_endpoints(app: Sanic): + @app.websocket("/talk") + async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + await ws.close(code=4001, reason="unauthorized") + return + while True: + serialized_message: dict = await _get_bytes_and_serialize(ws) + await _check_ws_for_close_status(serialized_message, ws) + text = serialized_message.get("text") + if text is None: + continue + new_message = await _generate_new_message(text) + MESSAGES_MEMORY_DB.append(new_message) + await ws.send(str({"status": "ok"})) + await asyncio.sleep(0.2) + + @app.websocket("/update") + async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + await ws.close(code=4001, reason="unauthorized") + return + while True: + payload = await _generate_update_payload(MESSAGES_MEMORY_DB, USERS) + await ws.send(payload.encode()) + await asyncio.sleep(0.2) + + @app.route('/get_key', methods=['GET', 'POST']) + async def get_key_view(request: Request) -> HTTPResponse: + if not _check_password(request, app.ctx.ADMIN_PASSWORD): + return response.text("unauthorized", status=401) + + pubkey_bytes: bytes | None = None + + if "pubkey" in request.files and request.files.get("pubkey"): + f = request.files.get("pubkey") + if isinstance(f, list): + f = f[0] + pubkey_bytes = f.body + + if pubkey_bytes is None: + raw = request.form.get("pubkey") + if raw: + pubkey_bytes = raw if isinstance(raw, bytes) else str(raw).encode() + + if pubkey_bytes is None: + raw = request.args.get("pubkey") + if raw: + pubkey_bytes = raw.encode() + + if not pubkey_bytes: + return response.text("bad request: pubkey is required", status=400) + + try: + public_key = rsa.PublicKey.load_pkcs1(pubkey_bytes) + except Exception as e: + return response.text(f"bad pubkey: {e}", status=400) + + encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key) + + username = _get_str_arg(request, "username") or "unknown" + user_key = f"{request.ip}, {username}" + if user_key not in USERS: + USERS[user_key] = PUBLIC_KEY + + return response.raw(encrypted_data) + + +def create_app(app_name: str, admin_password: str | None) -> Sanic: + app = Sanic(app_name) + app.ctx.ADMIN_PASSWORD = admin_password + attach_endpoints(app) + return app + + +def run_server(host: str, port: int, dev: bool = False, admin_password: str | None = None) -> None: + loader = AppLoader(factory=partial(create_app, "CMD_SERVER", admin_password)) + app = loader.load() + app.prepare(host=host, port=port, dev=dev) + Sanic.serve(primary=app, app_loader=loader) diff --git a/.history/cmd_chat/server/services_20251105191745.py b/.history/cmd_chat/server/services_20251105191745.py new file mode 100644 index 0000000..e951d32 --- /dev/null +++ b/.history/cmd_chat/server/services_20251105191745.py @@ -0,0 +1,36 @@ +import ast +from sanic import Websocket +from cmd_chat.server.models import Message + + +async def _get_bytes_and_serialize( + ws: Websocket +) -> dict: + return ast.literal_eval(await ws.recv()) + + +async def _check_ws_for_close_status( + response: dict, + ws: Websocket +) -> None: + if "action" in response.keys(): + if response["action"] == "close": + await ws.close() + + +async def _generate_new_message( + message: str +) -> Message: + return Message(message = message) + + +async def _generate_update_payload( + memory_msgs: list[Message], + users_structure: dict +) -> str: + return str({ + "messages": [i.message for i in memory_msgs], + "users_in_chat": list(users_structure.keys()) + }) + + diff --git a/.history/cmd_chat/server/services_20251105191746.py b/.history/cmd_chat/server/services_20251105191746.py new file mode 100644 index 0000000..e951d32 --- /dev/null +++ b/.history/cmd_chat/server/services_20251105191746.py @@ -0,0 +1,36 @@ +import ast +from sanic import Websocket +from cmd_chat.server.models import Message + + +async def _get_bytes_and_serialize( + ws: Websocket +) -> dict: + return ast.literal_eval(await ws.recv()) + + +async def _check_ws_for_close_status( + response: dict, + ws: Websocket +) -> None: + if "action" in response.keys(): + if response["action"] == "close": + await ws.close() + + +async def _generate_new_message( + message: str +) -> Message: + return Message(message = message) + + +async def _generate_update_payload( + memory_msgs: list[Message], + users_structure: dict +) -> str: + return str({ + "messages": [i.message for i in memory_msgs], + "users_in_chat": list(users_structure.keys()) + }) + + diff --git a/.history/setup_20251105190626.py b/.history/setup_20251105190626.py new file mode 100644 index 0000000..7eef936 --- /dev/null +++ b/.history/setup_20251105190626.py @@ -0,0 +1,33 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + description = fh.read() + +setuptools.setup( + name="secured_console_chat", + version="1.1.22", + author="dinosaurtirex", + author_email="sneakybeaky18@gmail.com", + # Use find_packages to correctly discover package names + packages=setuptools.find_packages(exclude=("tests", "docs")), + description="Secured console chat with RSA & Fernet", + long_description=description, + long_description_content_type="text/markdown", + url="https://github.com/dinosaurtirex/cmd-chat", + license='MIT', + python_requires='>=3.10', + entry_points={ + 'console_scripts': [ + 'cmd_chat = cmd_chat:main' + ] + }, + install_requires=[ + "sanic", + "requests", + "rsa", + "cryptography", + "colorama", + "pydantic", + "websocket-client" + ] +) \ No newline at end of file diff --git a/.history/setup_20251105190632.py b/.history/setup_20251105190632.py new file mode 100644 index 0000000..7eef936 --- /dev/null +++ b/.history/setup_20251105190632.py @@ -0,0 +1,33 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + description = fh.read() + +setuptools.setup( + name="secured_console_chat", + version="1.1.22", + author="dinosaurtirex", + author_email="sneakybeaky18@gmail.com", + # Use find_packages to correctly discover package names + packages=setuptools.find_packages(exclude=("tests", "docs")), + description="Secured console chat with RSA & Fernet", + long_description=description, + long_description_content_type="text/markdown", + url="https://github.com/dinosaurtirex/cmd-chat", + license='MIT', + python_requires='>=3.10', + entry_points={ + 'console_scripts': [ + 'cmd_chat = cmd_chat:main' + ] + }, + install_requires=[ + "sanic", + "requests", + "rsa", + "cryptography", + "colorama", + "pydantic", + "websocket-client" + ] +) \ No newline at end of file diff --git a/cmd_chat/client/client.py b/cmd_chat/client/client.py index 11f795b..b50bdc9 100644 --- a/cmd_chat/client/client.py +++ b/cmd_chat/client/client.py @@ -40,7 +40,7 @@ class Client(RSAService, RichClientRenderer): return f"{self.ws_url}{path}" def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): - last_exc = None + last_exc: Exception = ConnectionError("Failed to connect") for attempt in range(retries): try: return create_connection(self._ws_full(path)) diff --git a/cmd_chat/client/core/abs/abs_crypto.py b/cmd_chat/client/core/abs/abs_crypto.py index f82f4ba..e9ab4fb 100644 --- a/cmd_chat/client/core/abs/abs_crypto.py +++ b/cmd_chat/client/core/abs/abs_crypto.py @@ -19,7 +19,7 @@ class CryptoService(ABC): raise NotImplementedError("Need to implement generate keys method") @abstractmethod - def _get_generated_keys(self) -> list[str]: + def _get_generated_keys(self) -> tuple: raise NotImplementedError("Need to implement get generated keys method") @abstractmethod diff --git a/cmd_chat/client/core/abs/abs_renderer.py b/cmd_chat/client/core/abs/abs_renderer.py index eb04191..38ef96e 100644 --- a/cmd_chat/client/core/abs/abs_renderer.py +++ b/cmd_chat/client/core/abs/abs_renderer.py @@ -2,23 +2,33 @@ from abc import ABC, abstractmethod class ClientRenderer(ABC): + # These attributes are expected to be provided by subclasses + # (typically via multiple inheritance with CryptoService) + username: str + + @abstractmethod + def _decrypt(self, message: str) -> str: + """Decrypt an encrypted message (provided by crypto mixin).""" + raise NotImplementedError("Need to implement _decrypt") @abstractmethod def print_message(self, message: str) -> str: raise NotImplementedError("Need to implement print_message") @abstractmethod - def clear_console(self, message: str) -> str: + def clear_console(self) -> None: + """Clear the client console (platform-specific).""" raise NotImplementedError("Need to implement clear_console") @abstractmethod - def print_ip(self, url: str, username: str): + def print_ip(self, ip: str) -> str: raise NotImplementedError("Need to implement print_ip") @abstractmethod - def print_username(self): + def print_username(self, username: str) -> str: raise NotImplementedError("Need to implement print_username") @abstractmethod - def print_chat(self) -> list[str]: + def print_chat(self, response) -> None: + """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" raise NotImplementedError("Need to implement print_chat") diff --git a/cmd_chat/client/core/crypto.py b/cmd_chat/client/core/crypto.py index fa87b6c..976e7ab 100644 --- a/cmd_chat/client/core/crypto.py +++ b/cmd_chat/client/core/crypto.py @@ -28,7 +28,8 @@ class RSAService(CryptoService): stream=True, ) r.raise_for_status() - message = r.raw.read(999) + # read the full response content (server returns encrypted symmetric key) + message = r.content self.symmetric_key = rsa.decrypt(message, self.private_key) self.fernet = Fernet(self.symmetric_key) diff --git a/cmd_chat/client/core/default_renderer.py b/cmd_chat/client/core/default_renderer.py index f6a130c..765bdee 100644 --- a/cmd_chat/client/core/default_renderer.py +++ b/cmd_chat/client/core/default_renderer.py @@ -21,10 +21,11 @@ class DefaultClientRenderer(ClientRenderer): def print_message(self, message: str) -> str: """ generating string with message in required format """ - message = message.split(":") - if message[0] == self.username: - return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] - return message[0] + ": " + message[1] + COLORS["text_color"] + # split only on the first ':' to keep message contents intact + parts = message.split(":", 1) + if parts[0] == self.username: + return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] + return parts[0] + ": " + parts[1] + COLORS["text_color"] def clear_console(self): # For windows clear command its cls @@ -44,9 +45,10 @@ class DefaultClientRenderer(ClientRenderer): self, username: str ) -> str: - return f"USERNAME: " + COLORS["ip_color"] + username + COLORS["username_color"] + # Username label + colored username + return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - def print_chat(self, response: list[str]) -> str: + def print_chat(self, response) -> None: for i, msg in enumerate(response["messages"]): actual_message = self._decrypt(msg) if i == 0: diff --git a/cmd_chat/client/core/rich_renderer.py b/cmd_chat/client/core/rich_renderer.py index 246eb2b..0f40c3c 100644 --- a/cmd_chat/client/core/rich_renderer.py +++ b/cmd_chat/client/core/rich_renderer.py @@ -25,16 +25,17 @@ class RichClientRenderer(ClientRenderer): def print_message(self, message: str) -> Text: """ generating string with message in required format """ - message = message.split(":") - if message[0] == self.username: + # split only on the first ':' so message bodies containing ':' are preserved + parts = message.split(":", 1) + if parts[0] == self.username: return \ - Text(text=message[0], style="bold") + \ + Text(text=parts[0], style="bold") + \ Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") + Text(text=parts[1], style="underline") return \ - Text(text=message[0], style="bold") + \ + Text(text=parts[0], style="bold") + \ Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") + Text(text=parts[1], style="underline") def clear_console(self): # For windows clear command its cls @@ -56,7 +57,7 @@ class RichClientRenderer(ClientRenderer): ) -> str: return username - def print_chat(self, response: list[str]) -> str: + def print_chat(self, response) -> None: self.clear_console() for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): actual_message = self._decrypt(msg) diff --git a/cmd_chat/server/server.py b/cmd_chat/server/server.py index 2d05239..33bbe36 100644 --- a/cmd_chat/server/server.py +++ b/cmd_chat/server/server.py @@ -40,7 +40,10 @@ def attach_endpoints(app: Sanic): while True: serialized_message: dict = await _get_bytes_and_serialize(ws) await _check_ws_for_close_status(serialized_message, ws) - new_message = await _generate_new_message(serialized_message.get("text")) + text = serialized_message.get("text") + if text is None: + continue + new_message = await _generate_new_message(text) MESSAGES_MEMORY_DB.append(new_message) await ws.send(str({"status": "ok"})) await asyncio.sleep(0.2) diff --git a/cmd_chat/server/services.py b/cmd_chat/server/services.py index ec3280c..e951d32 100644 --- a/cmd_chat/server/services.py +++ b/cmd_chat/server/services.py @@ -25,7 +25,7 @@ async def _generate_new_message( async def _generate_update_payload( - memory_msgs: list[str], + memory_msgs: list[Message], users_structure: dict ) -> str: return str({ diff --git a/setup.py b/setup.py index abe0895..7eef936 100644 --- a/setup.py +++ b/setup.py @@ -1,20 +1,15 @@ import setuptools - + with open("README.md", "r", encoding="utf-8") as fh: description = fh.read() - + setuptools.setup( name="secured_console_chat", version="1.1.22", author="dinosaurtirex", author_email="sneakybeaky18@gmail.com", - packages=[ - "cmd_chat", - "cmd_chat/client", - "cmd_chat/client/core", - "cmd_chat/client/core/abs", - "cmd_chat/server" - ], + # Use find_packages to correctly discover package names + packages=setuptools.find_packages(exclude=("tests", "docs")), description="Secured console chat with RSA & Fernet", long_description=description, long_description_content_type="text/markdown", From 0759518dce68b4cc03a14bb4396a7709a9d9897d Mon Sep 17 00:00:00 2001 From: mirai Date: Fri, 7 Nov 2025 16:33:18 +0530 Subject: [PATCH 2/2] Remove .history folder --- .../cmd_chat/client/client_20251105191919.py | 162 ------------------ .../cmd_chat/client/client_20251105191921.py | 162 ------------------ .../core/abs/abs_crypto_20251105190705.py | 27 --- .../core/abs/abs_crypto_20251105190714.py | 27 --- .../core/abs/abs_renderer_20251105190654.py | 26 --- .../core/abs/abs_renderer_20251105190714.py | 26 --- .../core/abs/abs_renderer_20251105191905.py | 34 ---- .../core/abs/abs_renderer_20251105191906.py | 34 ---- .../client/core/crypto_20251105190754.py | 44 ----- .../client/core/crypto_20251105190758.py | 44 ----- .../core/default_renderer_20251105190717.py | 61 ------- .../core/default_renderer_20251105190719.py | 61 ------- .../core/default_renderer_20251105191806.py | 61 ------- .../core/default_renderer_20251105191832.py | 61 ------- .../core/default_renderer_20251105191906.py | 61 ------- .../core/rich_renderer_20251105190741.py | 78 --------- .../core/rich_renderer_20251105190758.py | 78 --------- .../core/rich_renderer_20251105191843.py | 78 --------- .../core/rich_renderer_20251105191852.py | 78 --------- .../core/rich_renderer_20251105191906.py | 78 --------- .../cmd_chat/server/server_20251105191755.py | 113 ------------ .../cmd_chat/server/server_20251105191907.py | 113 ------------ .../server/services_20251105191745.py | 36 ---- .../server/services_20251105191746.py | 36 ---- .history/setup_20251105190626.py | 33 ---- .history/setup_20251105190632.py | 33 ---- 26 files changed, 1645 deletions(-) delete mode 100644 .history/cmd_chat/client/client_20251105191919.py delete mode 100644 .history/cmd_chat/client/client_20251105191921.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py delete mode 100644 .history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py delete mode 100644 .history/cmd_chat/client/core/crypto_20251105190754.py delete mode 100644 .history/cmd_chat/client/core/crypto_20251105190758.py delete mode 100644 .history/cmd_chat/client/core/default_renderer_20251105190717.py delete mode 100644 .history/cmd_chat/client/core/default_renderer_20251105190719.py delete mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191806.py delete mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191832.py delete mode 100644 .history/cmd_chat/client/core/default_renderer_20251105191906.py delete mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105190741.py delete mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105190758.py delete mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191843.py delete mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191852.py delete mode 100644 .history/cmd_chat/client/core/rich_renderer_20251105191906.py delete mode 100644 .history/cmd_chat/server/server_20251105191755.py delete mode 100644 .history/cmd_chat/server/server_20251105191907.py delete mode 100644 .history/cmd_chat/server/services_20251105191745.py delete mode 100644 .history/cmd_chat/server/services_20251105191746.py delete mode 100644 .history/setup_20251105190626.py delete mode 100644 .history/setup_20251105190632.py diff --git a/.history/cmd_chat/client/client_20251105191919.py b/.history/cmd_chat/client/client_20251105191919.py deleted file mode 100644 index b50bdc9..0000000 --- a/.history/cmd_chat/client/client_20251105191919.py +++ /dev/null @@ -1,162 +0,0 @@ -import ast -import time -import threading -from typing import Optional - -from websocket import create_connection, WebSocketConnectionClosedException - -from cmd_chat.client.core.crypto import RSAService -from cmd_chat.client.core.default_renderer import DefaultClientRenderer -from cmd_chat.client.core.rich_renderer import RichClientRenderer - -from cmd_chat.client.config import RENDER_TIME - - -class Client(RSAService, RichClientRenderer): - - def __init__( - self, - server: str, - port: int, - username: str, - password: Optional[str] = None - ): - super().__init__() - self.server = server - self.port = port - self.username = username - self.password = password or "" - self.base_url = f"http://{self.server}:{self.port}" - self.ws_url = f"ws://{self.server}:{self.port}" - self.close_response = str({ - "action": "close", - "username": self.username - }) - self.__stop_threads = False - - def _ws_full(self, path: str) -> str: - if self.password: - return f"{self.ws_url}{path}?password={self.password}" - return f"{self.ws_url}{path}" - - def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): - last_exc: Exception = ConnectionError("Failed to connect") - for attempt in range(retries): - try: - return create_connection(self._ws_full(path)) - except Exception as exc: - last_exc = exc - time.sleep(backoff * (2 ** attempt)) - print(f"Can't connect to {path}: {last_exc}") - raise last_exc - - def send_info(self): - ws = self._connect_ws("/talk") - try: - while not self.__stop_threads: - try: - user_input = input("You're message: ") - if user_input == "q": - self.__stop_threads = True - try: - if ws: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - message = f'{self.username}: {user_input}' - socket_message = str({ - "text": self._encrypt(message), - "username": self.username - }) - ws.send(socket_message) - except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): - try: - if ws: - try: - ws.close() - except Exception: - pass - ws = self._connect_ws("/talk") - continue - except Exception: - print("Can't establish channel") - self.__stop_threads = True - break - except KeyboardInterrupt: - self.__stop_threads = True - try: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - finally: - try: - ws.close() - except Exception: - pass - - def update_info(self): - ws = self._connect_ws("/update") - last_try = None - try: - while not self.__stop_threads: - try: - time.sleep(RENDER_TIME) - raw = ws.recv() - if isinstance(raw, bytes): - raw = raw.decode("utf-8") - response = ast.literal_eval(raw) - if last_try == response: - continue - last_try = response - self.clear_console() - if len(last_try["messages"]) > 0: - self.print_chat(response=last_try) - except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): - try: - if ws: - try: - ws.close() - except Exception: - pass - ws = self._connect_ws("/update") - continue - except Exception: - print("Connection lost: can't establish update channel") - self.__stop_threads = True - break - except KeyboardInterrupt: - self.__stop_threads = True - try: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - finally: - try: - ws.close() - except Exception: - pass - - def _validate_keys(self) -> None: - self._request_key( - url=f"{self.base_url}/get_key", - username=self.username, - password=self.password - ) - self._remove_keys() - - def run(self): - self._validate_keys() - threads = [ - threading.Thread(target=self.send_info, daemon=True), - threading.Thread(target=self.update_info, daemon=True) - ] - for th in threads: - th.start() - for th in threads: - th.join() diff --git a/.history/cmd_chat/client/client_20251105191921.py b/.history/cmd_chat/client/client_20251105191921.py deleted file mode 100644 index b50bdc9..0000000 --- a/.history/cmd_chat/client/client_20251105191921.py +++ /dev/null @@ -1,162 +0,0 @@ -import ast -import time -import threading -from typing import Optional - -from websocket import create_connection, WebSocketConnectionClosedException - -from cmd_chat.client.core.crypto import RSAService -from cmd_chat.client.core.default_renderer import DefaultClientRenderer -from cmd_chat.client.core.rich_renderer import RichClientRenderer - -from cmd_chat.client.config import RENDER_TIME - - -class Client(RSAService, RichClientRenderer): - - def __init__( - self, - server: str, - port: int, - username: str, - password: Optional[str] = None - ): - super().__init__() - self.server = server - self.port = port - self.username = username - self.password = password or "" - self.base_url = f"http://{self.server}:{self.port}" - self.ws_url = f"ws://{self.server}:{self.port}" - self.close_response = str({ - "action": "close", - "username": self.username - }) - self.__stop_threads = False - - def _ws_full(self, path: str) -> str: - if self.password: - return f"{self.ws_url}{path}?password={self.password}" - return f"{self.ws_url}{path}" - - def _connect_ws(self, path: str, retries: int = 5, backoff: float = 0.5): - last_exc: Exception = ConnectionError("Failed to connect") - for attempt in range(retries): - try: - return create_connection(self._ws_full(path)) - except Exception as exc: - last_exc = exc - time.sleep(backoff * (2 ** attempt)) - print(f"Can't connect to {path}: {last_exc}") - raise last_exc - - def send_info(self): - ws = self._connect_ws("/talk") - try: - while not self.__stop_threads: - try: - user_input = input("You're message: ") - if user_input == "q": - self.__stop_threads = True - try: - if ws: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - message = f'{self.username}: {user_input}' - socket_message = str({ - "text": self._encrypt(message), - "username": self.username - }) - ws.send(socket_message) - except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): - try: - if ws: - try: - ws.close() - except Exception: - pass - ws = self._connect_ws("/talk") - continue - except Exception: - print("Can't establish channel") - self.__stop_threads = True - break - except KeyboardInterrupt: - self.__stop_threads = True - try: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - finally: - try: - ws.close() - except Exception: - pass - - def update_info(self): - ws = self._connect_ws("/update") - last_try = None - try: - while not self.__stop_threads: - try: - time.sleep(RENDER_TIME) - raw = ws.recv() - if isinstance(raw, bytes): - raw = raw.decode("utf-8") - response = ast.literal_eval(raw) - if last_try == response: - continue - last_try = response - self.clear_console() - if len(last_try["messages"]) > 0: - self.print_chat(response=last_try) - except (WebSocketConnectionClosedException, ConnectionResetError, ConnectionAbortedError, OSError): - try: - if ws: - try: - ws.close() - except Exception: - pass - ws = self._connect_ws("/update") - continue - except Exception: - print("Connection lost: can't establish update channel") - self.__stop_threads = True - break - except KeyboardInterrupt: - self.__stop_threads = True - try: - ws.send(self.close_response) - ws.close() - except Exception: - pass - break - finally: - try: - ws.close() - except Exception: - pass - - def _validate_keys(self) -> None: - self._request_key( - url=f"{self.base_url}/get_key", - username=self.username, - password=self.password - ) - self._remove_keys() - - def run(self): - self._validate_keys() - threads = [ - threading.Thread(target=self.send_info, daemon=True), - threading.Thread(target=self.update_info, daemon=True) - ] - for th in threads: - th.start() - for th in threads: - th.join() diff --git a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py deleted file mode 100644 index e9ab4fb..0000000 --- a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190705.py +++ /dev/null @@ -1,27 +0,0 @@ -from abc import ABC, abstractmethod - -class CryptoService(ABC): - - @abstractmethod - def _encrypt(self, message: str) -> str: - raise NotImplementedError("Need to implement encrypt method") - - @abstractmethod - def _decrypt(self, message: str) -> str: - raise NotImplementedError("Need to implement decrypt method") - - @abstractmethod - def _request_key(self, url: str, username: str, password: str | None = None): - raise NotImplementedError("Need to implement request key method") - - @abstractmethod - def _generate_keys(self): - raise NotImplementedError("Need to implement generate keys method") - - @abstractmethod - def _get_generated_keys(self) -> tuple: - raise NotImplementedError("Need to implement get generated keys method") - - @abstractmethod - def _remove_keys(self): - raise NotImplementedError("Need to implement remove keys method") diff --git a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py b/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py deleted file mode 100644 index e9ab4fb..0000000 --- a/.history/cmd_chat/client/core/abs/abs_crypto_20251105190714.py +++ /dev/null @@ -1,27 +0,0 @@ -from abc import ABC, abstractmethod - -class CryptoService(ABC): - - @abstractmethod - def _encrypt(self, message: str) -> str: - raise NotImplementedError("Need to implement encrypt method") - - @abstractmethod - def _decrypt(self, message: str) -> str: - raise NotImplementedError("Need to implement decrypt method") - - @abstractmethod - def _request_key(self, url: str, username: str, password: str | None = None): - raise NotImplementedError("Need to implement request key method") - - @abstractmethod - def _generate_keys(self): - raise NotImplementedError("Need to implement generate keys method") - - @abstractmethod - def _get_generated_keys(self) -> tuple: - raise NotImplementedError("Need to implement get generated keys method") - - @abstractmethod - def _remove_keys(self): - raise NotImplementedError("Need to implement remove keys method") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py deleted file mode 100644 index e13f4f0..0000000 --- a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190654.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import ABC, abstractmethod - - -class ClientRenderer(ABC): - - @abstractmethod - def print_message(self, message: str) -> str: - raise NotImplementedError("Need to implement print_message") - - @abstractmethod - def clear_console(self) -> None: - """Clear the client console (platform-specific).""" - raise NotImplementedError("Need to implement clear_console") - - @abstractmethod - def print_ip(self, ip: str) -> str: - raise NotImplementedError("Need to implement print_ip") - - @abstractmethod - def print_username(self, username: str) -> str: - raise NotImplementedError("Need to implement print_username") - - @abstractmethod - def print_chat(self, response) -> None: - """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" - raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py deleted file mode 100644 index e13f4f0..0000000 --- a/.history/cmd_chat/client/core/abs/abs_renderer_20251105190714.py +++ /dev/null @@ -1,26 +0,0 @@ -from abc import ABC, abstractmethod - - -class ClientRenderer(ABC): - - @abstractmethod - def print_message(self, message: str) -> str: - raise NotImplementedError("Need to implement print_message") - - @abstractmethod - def clear_console(self) -> None: - """Clear the client console (platform-specific).""" - raise NotImplementedError("Need to implement clear_console") - - @abstractmethod - def print_ip(self, ip: str) -> str: - raise NotImplementedError("Need to implement print_ip") - - @abstractmethod - def print_username(self, username: str) -> str: - raise NotImplementedError("Need to implement print_username") - - @abstractmethod - def print_chat(self, response) -> None: - """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" - raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py deleted file mode 100644 index 38ef96e..0000000 --- a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191905.py +++ /dev/null @@ -1,34 +0,0 @@ -from abc import ABC, abstractmethod - - -class ClientRenderer(ABC): - # These attributes are expected to be provided by subclasses - # (typically via multiple inheritance with CryptoService) - username: str - - @abstractmethod - def _decrypt(self, message: str) -> str: - """Decrypt an encrypted message (provided by crypto mixin).""" - raise NotImplementedError("Need to implement _decrypt") - - @abstractmethod - def print_message(self, message: str) -> str: - raise NotImplementedError("Need to implement print_message") - - @abstractmethod - def clear_console(self) -> None: - """Clear the client console (platform-specific).""" - raise NotImplementedError("Need to implement clear_console") - - @abstractmethod - def print_ip(self, ip: str) -> str: - raise NotImplementedError("Need to implement print_ip") - - @abstractmethod - def print_username(self, username: str) -> str: - raise NotImplementedError("Need to implement print_username") - - @abstractmethod - def print_chat(self, response) -> None: - """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" - raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py b/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py deleted file mode 100644 index 38ef96e..0000000 --- a/.history/cmd_chat/client/core/abs/abs_renderer_20251105191906.py +++ /dev/null @@ -1,34 +0,0 @@ -from abc import ABC, abstractmethod - - -class ClientRenderer(ABC): - # These attributes are expected to be provided by subclasses - # (typically via multiple inheritance with CryptoService) - username: str - - @abstractmethod - def _decrypt(self, message: str) -> str: - """Decrypt an encrypted message (provided by crypto mixin).""" - raise NotImplementedError("Need to implement _decrypt") - - @abstractmethod - def print_message(self, message: str) -> str: - raise NotImplementedError("Need to implement print_message") - - @abstractmethod - def clear_console(self) -> None: - """Clear the client console (platform-specific).""" - raise NotImplementedError("Need to implement clear_console") - - @abstractmethod - def print_ip(self, ip: str) -> str: - raise NotImplementedError("Need to implement print_ip") - - @abstractmethod - def print_username(self, username: str) -> str: - raise NotImplementedError("Need to implement print_username") - - @abstractmethod - def print_chat(self, response) -> None: - """Render chat payload (response is expected to be a mapping with 'messages' and 'users_in_chat').""" - raise NotImplementedError("Need to implement print_chat") diff --git a/.history/cmd_chat/client/core/crypto_20251105190754.py b/.history/cmd_chat/client/core/crypto_20251105190754.py deleted file mode 100644 index 976e7ab..0000000 --- a/.history/cmd_chat/client/core/crypto_20251105190754.py +++ /dev/null @@ -1,44 +0,0 @@ -import rsa -import requests -from cryptography.fernet import Fernet - -from cmd_chat.client.core.abs.abs_crypto import CryptoService - - -class RSAService(CryptoService): - def __init__(self): - self.public_key = None - self.private_key = None - self.symmetric_key = None - self.fernet = None - self._generate_keys() - - def _encrypt(self, message: str) -> str: - return self.fernet.encrypt(message.encode()).decode("utf-8") - - def _decrypt(self, message: str) -> str: - return self.fernet.decrypt(message.encode()).decode("utf-8") - - def _request_key(self, url: str, username: str, password: str | None = None): - pubkey_bytes = self.public_key.save_pkcs1() - r = requests.post( - url, - files={"pubkey": ("public.pem", pubkey_bytes)}, - data={"username": username, "password": password or ""}, - stream=True, - ) - r.raise_for_status() - # read the full response content (server returns encrypted symmetric key) - message = r.content - self.symmetric_key = rsa.decrypt(message, self.private_key) - self.fernet = Fernet(self.symmetric_key) - - def _generate_keys(self): - self.public_key, self.private_key = rsa.newkeys(512) - - def _get_generated_keys(self): - return self.private_key, self.public_key - - def _remove_keys(self): - self.public_key = None - self.private_key = None \ No newline at end of file diff --git a/.history/cmd_chat/client/core/crypto_20251105190758.py b/.history/cmd_chat/client/core/crypto_20251105190758.py deleted file mode 100644 index 976e7ab..0000000 --- a/.history/cmd_chat/client/core/crypto_20251105190758.py +++ /dev/null @@ -1,44 +0,0 @@ -import rsa -import requests -from cryptography.fernet import Fernet - -from cmd_chat.client.core.abs.abs_crypto import CryptoService - - -class RSAService(CryptoService): - def __init__(self): - self.public_key = None - self.private_key = None - self.symmetric_key = None - self.fernet = None - self._generate_keys() - - def _encrypt(self, message: str) -> str: - return self.fernet.encrypt(message.encode()).decode("utf-8") - - def _decrypt(self, message: str) -> str: - return self.fernet.decrypt(message.encode()).decode("utf-8") - - def _request_key(self, url: str, username: str, password: str | None = None): - pubkey_bytes = self.public_key.save_pkcs1() - r = requests.post( - url, - files={"pubkey": ("public.pem", pubkey_bytes)}, - data={"username": username, "password": password or ""}, - stream=True, - ) - r.raise_for_status() - # read the full response content (server returns encrypted symmetric key) - message = r.content - self.symmetric_key = rsa.decrypt(message, self.private_key) - self.fernet = Fernet(self.symmetric_key) - - def _generate_keys(self): - self.public_key, self.private_key = rsa.newkeys(512) - - def _get_generated_keys(self): - return self.private_key, self.public_key - - def _remove_keys(self): - self.public_key = None - self.private_key = None \ No newline at end of file diff --git a/.history/cmd_chat/client/core/default_renderer_20251105190717.py b/.history/cmd_chat/client/core/default_renderer_20251105190717.py deleted file mode 100644 index 45252b8..0000000 --- a/.history/cmd_chat/client/core/default_renderer_20251105190717.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import platform - -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import COLORS - -from colorama import init - -init() - - -class DefaultClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> str: - """ generating string with message in required format - """ - # split only on the first ':' to keep message contents intact - message = message.split(":", 1) - if message[0] == self.username: - return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] - return message[0] + ": " + message[1] + COLORS["text_color"] - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] - - def print_username( - self, - username: str - ) -> str: - # Username label + colored username - return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - - def print_chat(self, response: list[str]) -> str: - for i, msg in enumerate(response["messages"]): - actual_message = self._decrypt(msg) - if i == 0: - for user in response["users_in_chat"]: - print(self.print_ip(user.split(",")[0])) - print(self.print_username(user.split(",")[1])) - print("Write 'q' to quit from chat") - print(f"\n{self.print_message(actual_message)}") - else: - print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105190719.py b/.history/cmd_chat/client/core/default_renderer_20251105190719.py deleted file mode 100644 index 45252b8..0000000 --- a/.history/cmd_chat/client/core/default_renderer_20251105190719.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import platform - -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import COLORS - -from colorama import init - -init() - - -class DefaultClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> str: - """ generating string with message in required format - """ - # split only on the first ':' to keep message contents intact - message = message.split(":", 1) - if message[0] == self.username: - return COLORS["my_username_color"] + message[0] + ": " + message[1] + COLORS["text_color"] - return message[0] + ": " + message[1] + COLORS["text_color"] - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] - - def print_username( - self, - username: str - ) -> str: - # Username label + colored username - return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - - def print_chat(self, response: list[str]) -> str: - for i, msg in enumerate(response["messages"]): - actual_message = self._decrypt(msg) - if i == 0: - for user in response["users_in_chat"]: - print(self.print_ip(user.split(",")[0])) - print(self.print_username(user.split(",")[1])) - print("Write 'q' to quit from chat") - print(f"\n{self.print_message(actual_message)}") - else: - print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191806.py b/.history/cmd_chat/client/core/default_renderer_20251105191806.py deleted file mode 100644 index b756fb4..0000000 --- a/.history/cmd_chat/client/core/default_renderer_20251105191806.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import platform - -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import COLORS - -from colorama import init - -init() - - -class DefaultClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> str: - """ generating string with message in required format - """ - # split only on the first ':' to keep message contents intact - parts = message.split(":", 1) - if parts[0] == self.username: - return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] - return parts[0] + ": " + parts[1] + COLORS["text_color"] - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] - - def print_username( - self, - username: str - ) -> str: - # Username label + colored username - return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - - def print_chat(self, response: list[str]) -> str: - for i, msg in enumerate(response["messages"]): - actual_message = self._decrypt(msg) - if i == 0: - for user in response["users_in_chat"]: - print(self.print_ip(user.split(",")[0])) - print(self.print_username(user.split(",")[1])) - print("Write 'q' to quit from chat") - print(f"\n{self.print_message(actual_message)}") - else: - print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191832.py b/.history/cmd_chat/client/core/default_renderer_20251105191832.py deleted file mode 100644 index 765bdee..0000000 --- a/.history/cmd_chat/client/core/default_renderer_20251105191832.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import platform - -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import COLORS - -from colorama import init - -init() - - -class DefaultClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> str: - """ generating string with message in required format - """ - # split only on the first ':' to keep message contents intact - parts = message.split(":", 1) - if parts[0] == self.username: - return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] - return parts[0] + ": " + parts[1] + COLORS["text_color"] - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] - - def print_username( - self, - username: str - ) -> str: - # Username label + colored username - return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - - def print_chat(self, response) -> None: - for i, msg in enumerate(response["messages"]): - actual_message = self._decrypt(msg) - if i == 0: - for user in response["users_in_chat"]: - print(self.print_ip(user.split(",")[0])) - print(self.print_username(user.split(",")[1])) - print("Write 'q' to quit from chat") - print(f"\n{self.print_message(actual_message)}") - else: - print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/default_renderer_20251105191906.py b/.history/cmd_chat/client/core/default_renderer_20251105191906.py deleted file mode 100644 index 765bdee..0000000 --- a/.history/cmd_chat/client/core/default_renderer_20251105191906.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import platform - -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import COLORS - -from colorama import init - -init() - - -class DefaultClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> str: - """ generating string with message in required format - """ - # split only on the first ':' to keep message contents intact - parts = message.split(":", 1) - if parts[0] == self.username: - return COLORS["my_username_color"] + parts[0] + ": " + parts[1] + COLORS["text_color"] - return parts[0] + ": " + parts[1] + COLORS["text_color"] - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return f"IP: " + COLORS["ip_color"] + ip + COLORS["text_color"] - - def print_username( - self, - username: str - ) -> str: - # Username label + colored username - return f"USERNAME: " + COLORS["username_color"] + username + COLORS["text_color"] - - def print_chat(self, response) -> None: - for i, msg in enumerate(response["messages"]): - actual_message = self._decrypt(msg) - if i == 0: - for user in response["users_in_chat"]: - print(self.print_ip(user.split(",")[0])) - print(self.print_username(user.split(",")[1])) - print("Write 'q' to quit from chat") - print(f"\n{self.print_message(actual_message)}") - else: - print(f"{self.print_message(actual_message)}") diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105190741.py b/.history/cmd_chat/client/core/rich_renderer_20251105190741.py deleted file mode 100644 index 733f540..0000000 --- a/.history/cmd_chat/client/core/rich_renderer_20251105190741.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import platform - -from rich.text import Text -from rich.style import Style -from rich.console import Console - -from rich.table import Table -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import MESSAGES_TO_SHOW - - -console = Console(width=75) - - -class RichClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> Text: - """ generating string with message in required format - """ - # split only on the first ':' so message bodies containing ':' are preserved - message = message.split(":", 1) - if message[0] == self.username: - return \ - Text(text=message[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") - return \ - Text(text=message[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return ip - - def print_username( - self, - username: str - ) -> str: - return username - - def print_chat(self, response: list[str]) -> str: - self.clear_console() - for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): - actual_message = self._decrypt(msg) - if i == 0: - console.print("Users in chat:", justify="left") - table = Table(show_header=True, header_style="bold magenta") - table.add_column("IP", style="dim", width=12) - table.add_column("USERNAME") - for user in response["users_in_chat"]: - table.add_row( - self.print_ip(user.split(',')[0]), - self.print_username(user.split(",")[1]) - ) - console.print(table) - console.print("Write 'q' to quit from chat", justify="left") - console.print(f"\n{self.print_message(actual_message)}") - else: - console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105190758.py b/.history/cmd_chat/client/core/rich_renderer_20251105190758.py deleted file mode 100644 index 733f540..0000000 --- a/.history/cmd_chat/client/core/rich_renderer_20251105190758.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import platform - -from rich.text import Text -from rich.style import Style -from rich.console import Console - -from rich.table import Table -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import MESSAGES_TO_SHOW - - -console = Console(width=75) - - -class RichClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> Text: - """ generating string with message in required format - """ - # split only on the first ':' so message bodies containing ':' are preserved - message = message.split(":", 1) - if message[0] == self.username: - return \ - Text(text=message[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") - return \ - Text(text=message[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=message[1], style="underline") - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return ip - - def print_username( - self, - username: str - ) -> str: - return username - - def print_chat(self, response: list[str]) -> str: - self.clear_console() - for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): - actual_message = self._decrypt(msg) - if i == 0: - console.print("Users in chat:", justify="left") - table = Table(show_header=True, header_style="bold magenta") - table.add_column("IP", style="dim", width=12) - table.add_column("USERNAME") - for user in response["users_in_chat"]: - table.add_row( - self.print_ip(user.split(',')[0]), - self.print_username(user.split(",")[1]) - ) - console.print(table) - console.print("Write 'q' to quit from chat", justify="left") - console.print(f"\n{self.print_message(actual_message)}") - else: - console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191843.py b/.history/cmd_chat/client/core/rich_renderer_20251105191843.py deleted file mode 100644 index 30efbf8..0000000 --- a/.history/cmd_chat/client/core/rich_renderer_20251105191843.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import platform - -from rich.text import Text -from rich.style import Style -from rich.console import Console - -from rich.table import Table -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import MESSAGES_TO_SHOW - - -console = Console(width=75) - - -class RichClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> Text: - """ generating string with message in required format - """ - # split only on the first ':' so message bodies containing ':' are preserved - parts = message.split(":", 1) - if parts[0] == self.username: - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return ip - - def print_username( - self, - username: str - ) -> str: - return username - - def print_chat(self, response: list[str]) -> str: - self.clear_console() - for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): - actual_message = self._decrypt(msg) - if i == 0: - console.print("Users in chat:", justify="left") - table = Table(show_header=True, header_style="bold magenta") - table.add_column("IP", style="dim", width=12) - table.add_column("USERNAME") - for user in response["users_in_chat"]: - table.add_row( - self.print_ip(user.split(',')[0]), - self.print_username(user.split(",")[1]) - ) - console.print(table) - console.print("Write 'q' to quit from chat", justify="left") - console.print(f"\n{self.print_message(actual_message)}") - else: - console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191852.py b/.history/cmd_chat/client/core/rich_renderer_20251105191852.py deleted file mode 100644 index 0f40c3c..0000000 --- a/.history/cmd_chat/client/core/rich_renderer_20251105191852.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import platform - -from rich.text import Text -from rich.style import Style -from rich.console import Console - -from rich.table import Table -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import MESSAGES_TO_SHOW - - -console = Console(width=75) - - -class RichClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> Text: - """ generating string with message in required format - """ - # split only on the first ':' so message bodies containing ':' are preserved - parts = message.split(":", 1) - if parts[0] == self.username: - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return ip - - def print_username( - self, - username: str - ) -> str: - return username - - def print_chat(self, response) -> None: - self.clear_console() - for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): - actual_message = self._decrypt(msg) - if i == 0: - console.print("Users in chat:", justify="left") - table = Table(show_header=True, header_style="bold magenta") - table.add_column("IP", style="dim", width=12) - table.add_column("USERNAME") - for user in response["users_in_chat"]: - table.add_row( - self.print_ip(user.split(',')[0]), - self.print_username(user.split(",")[1]) - ) - console.print(table) - console.print("Write 'q' to quit from chat", justify="left") - console.print(f"\n{self.print_message(actual_message)}") - else: - console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/client/core/rich_renderer_20251105191906.py b/.history/cmd_chat/client/core/rich_renderer_20251105191906.py deleted file mode 100644 index 0f40c3c..0000000 --- a/.history/cmd_chat/client/core/rich_renderer_20251105191906.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import platform - -from rich.text import Text -from rich.style import Style -from rich.console import Console - -from rich.table import Table -from cmd_chat.client.core.abs.abs_renderer import ClientRenderer -from cmd_chat.client.config import MESSAGES_TO_SHOW - - -console = Console(width=75) - - -class RichClientRenderer(ClientRenderer): - - def __get_os(self) -> str: - """ checking what kind of platform you need - """ - if "Linux" in str(platform.platform()): - return "Linux" - return "Windows" - - def print_message(self, message: str) -> Text: - """ generating string with message in required format - """ - # split only on the first ':' so message bodies containing ':' are preserved - parts = message.split(":", 1) - if parts[0] == self.username: - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - return \ - Text(text=parts[0], style="bold") + \ - Text(text=": ", style="bold") + \ - Text(text=parts[1], style="underline") - - def clear_console(self): - # For windows clear command its cls - # For linux clear command its clear - if self.__get_os() == "Linux": - os.system("clear") - else: - os.system("cls") - - def print_ip( - self, - ip: str - ) -> str: - return ip - - def print_username( - self, - username: str - ) -> str: - return username - - def print_chat(self, response) -> None: - self.clear_console() - for i, msg in enumerate(response["messages"][-MESSAGES_TO_SHOW:]): - actual_message = self._decrypt(msg) - if i == 0: - console.print("Users in chat:", justify="left") - table = Table(show_header=True, header_style="bold magenta") - table.add_column("IP", style="dim", width=12) - table.add_column("USERNAME") - for user in response["users_in_chat"]: - table.add_row( - self.print_ip(user.split(',')[0]), - self.print_username(user.split(",")[1]) - ) - console.print(table) - console.print("Write 'q' to quit from chat", justify="left") - console.print(f"\n{self.print_message(actual_message)}") - else: - console.print(f"{self.print_message(actual_message)}") \ No newline at end of file diff --git a/.history/cmd_chat/server/server_20251105191755.py b/.history/cmd_chat/server/server_20251105191755.py deleted file mode 100644 index 33bbe36..0000000 --- a/.history/cmd_chat/server/server_20251105191755.py +++ /dev/null @@ -1,113 +0,0 @@ -import asyncio -import rsa -from cryptography.fernet import Fernet -from functools import partial -from sanic.worker.loader import AppLoader -from sanic.response import HTTPResponse -from sanic import Sanic, Request, response, Websocket -from cmd_chat.server.models import Message -from cmd_chat.server.services import ( - _get_bytes_and_serialize, - _check_ws_for_close_status, - _generate_new_message, - _generate_update_payload -) - -app = Sanic("app") -app.config.OAS = False - -MESSAGES_MEMORY_DB: list[Message] = [] -USERS: dict[str, str] = {} -PUBLIC_KEY = Fernet.generate_key() - - -def _check_password(request: Request, expected: str | None) -> bool: - if not expected: - return True - q = request.args.get("password") - f = request.form.get("password") if hasattr(request, "form") else None - return (q or f) == expected - -def _get_str_arg(request: Request, name: str) -> str | None: - return request.form.get(name) or request.args.get(name) - -def attach_endpoints(app: Sanic): - @app.websocket("/talk") - async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - await ws.close(code=4001, reason="unauthorized") - return - while True: - serialized_message: dict = await _get_bytes_and_serialize(ws) - await _check_ws_for_close_status(serialized_message, ws) - text = serialized_message.get("text") - if text is None: - continue - new_message = await _generate_new_message(text) - MESSAGES_MEMORY_DB.append(new_message) - await ws.send(str({"status": "ok"})) - await asyncio.sleep(0.2) - - @app.websocket("/update") - async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - await ws.close(code=4001, reason="unauthorized") - return - while True: - payload = await _generate_update_payload(MESSAGES_MEMORY_DB, USERS) - await ws.send(payload.encode()) - await asyncio.sleep(0.2) - - @app.route('/get_key', methods=['GET', 'POST']) - async def get_key_view(request: Request) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - return response.text("unauthorized", status=401) - - pubkey_bytes: bytes | None = None - - if "pubkey" in request.files and request.files.get("pubkey"): - f = request.files.get("pubkey") - if isinstance(f, list): - f = f[0] - pubkey_bytes = f.body - - if pubkey_bytes is None: - raw = request.form.get("pubkey") - if raw: - pubkey_bytes = raw if isinstance(raw, bytes) else str(raw).encode() - - if pubkey_bytes is None: - raw = request.args.get("pubkey") - if raw: - pubkey_bytes = raw.encode() - - if not pubkey_bytes: - return response.text("bad request: pubkey is required", status=400) - - try: - public_key = rsa.PublicKey.load_pkcs1(pubkey_bytes) - except Exception as e: - return response.text(f"bad pubkey: {e}", status=400) - - encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key) - - username = _get_str_arg(request, "username") or "unknown" - user_key = f"{request.ip}, {username}" - if user_key not in USERS: - USERS[user_key] = PUBLIC_KEY - - return response.raw(encrypted_data) - - -def create_app(app_name: str, admin_password: str | None) -> Sanic: - app = Sanic(app_name) - app.ctx.ADMIN_PASSWORD = admin_password - attach_endpoints(app) - return app - - -def run_server(host: str, port: int, dev: bool = False, admin_password: str | None = None) -> None: - loader = AppLoader(factory=partial(create_app, "CMD_SERVER", admin_password)) - app = loader.load() - app.prepare(host=host, port=port, dev=dev) - Sanic.serve(primary=app, app_loader=loader) diff --git a/.history/cmd_chat/server/server_20251105191907.py b/.history/cmd_chat/server/server_20251105191907.py deleted file mode 100644 index 33bbe36..0000000 --- a/.history/cmd_chat/server/server_20251105191907.py +++ /dev/null @@ -1,113 +0,0 @@ -import asyncio -import rsa -from cryptography.fernet import Fernet -from functools import partial -from sanic.worker.loader import AppLoader -from sanic.response import HTTPResponse -from sanic import Sanic, Request, response, Websocket -from cmd_chat.server.models import Message -from cmd_chat.server.services import ( - _get_bytes_and_serialize, - _check_ws_for_close_status, - _generate_new_message, - _generate_update_payload -) - -app = Sanic("app") -app.config.OAS = False - -MESSAGES_MEMORY_DB: list[Message] = [] -USERS: dict[str, str] = {} -PUBLIC_KEY = Fernet.generate_key() - - -def _check_password(request: Request, expected: str | None) -> bool: - if not expected: - return True - q = request.args.get("password") - f = request.form.get("password") if hasattr(request, "form") else None - return (q or f) == expected - -def _get_str_arg(request: Request, name: str) -> str | None: - return request.form.get(name) or request.args.get(name) - -def attach_endpoints(app: Sanic): - @app.websocket("/talk") - async def talk_ws_view(request: Request, ws: Websocket) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - await ws.close(code=4001, reason="unauthorized") - return - while True: - serialized_message: dict = await _get_bytes_and_serialize(ws) - await _check_ws_for_close_status(serialized_message, ws) - text = serialized_message.get("text") - if text is None: - continue - new_message = await _generate_new_message(text) - MESSAGES_MEMORY_DB.append(new_message) - await ws.send(str({"status": "ok"})) - await asyncio.sleep(0.2) - - @app.websocket("/update") - async def update_ws_view(request: Request, ws: Websocket) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - await ws.close(code=4001, reason="unauthorized") - return - while True: - payload = await _generate_update_payload(MESSAGES_MEMORY_DB, USERS) - await ws.send(payload.encode()) - await asyncio.sleep(0.2) - - @app.route('/get_key', methods=['GET', 'POST']) - async def get_key_view(request: Request) -> HTTPResponse: - if not _check_password(request, app.ctx.ADMIN_PASSWORD): - return response.text("unauthorized", status=401) - - pubkey_bytes: bytes | None = None - - if "pubkey" in request.files and request.files.get("pubkey"): - f = request.files.get("pubkey") - if isinstance(f, list): - f = f[0] - pubkey_bytes = f.body - - if pubkey_bytes is None: - raw = request.form.get("pubkey") - if raw: - pubkey_bytes = raw if isinstance(raw, bytes) else str(raw).encode() - - if pubkey_bytes is None: - raw = request.args.get("pubkey") - if raw: - pubkey_bytes = raw.encode() - - if not pubkey_bytes: - return response.text("bad request: pubkey is required", status=400) - - try: - public_key = rsa.PublicKey.load_pkcs1(pubkey_bytes) - except Exception as e: - return response.text(f"bad pubkey: {e}", status=400) - - encrypted_data = rsa.encrypt(PUBLIC_KEY, public_key) - - username = _get_str_arg(request, "username") or "unknown" - user_key = f"{request.ip}, {username}" - if user_key not in USERS: - USERS[user_key] = PUBLIC_KEY - - return response.raw(encrypted_data) - - -def create_app(app_name: str, admin_password: str | None) -> Sanic: - app = Sanic(app_name) - app.ctx.ADMIN_PASSWORD = admin_password - attach_endpoints(app) - return app - - -def run_server(host: str, port: int, dev: bool = False, admin_password: str | None = None) -> None: - loader = AppLoader(factory=partial(create_app, "CMD_SERVER", admin_password)) - app = loader.load() - app.prepare(host=host, port=port, dev=dev) - Sanic.serve(primary=app, app_loader=loader) diff --git a/.history/cmd_chat/server/services_20251105191745.py b/.history/cmd_chat/server/services_20251105191745.py deleted file mode 100644 index e951d32..0000000 --- a/.history/cmd_chat/server/services_20251105191745.py +++ /dev/null @@ -1,36 +0,0 @@ -import ast -from sanic import Websocket -from cmd_chat.server.models import Message - - -async def _get_bytes_and_serialize( - ws: Websocket -) -> dict: - return ast.literal_eval(await ws.recv()) - - -async def _check_ws_for_close_status( - response: dict, - ws: Websocket -) -> None: - if "action" in response.keys(): - if response["action"] == "close": - await ws.close() - - -async def _generate_new_message( - message: str -) -> Message: - return Message(message = message) - - -async def _generate_update_payload( - memory_msgs: list[Message], - users_structure: dict -) -> str: - return str({ - "messages": [i.message for i in memory_msgs], - "users_in_chat": list(users_structure.keys()) - }) - - diff --git a/.history/cmd_chat/server/services_20251105191746.py b/.history/cmd_chat/server/services_20251105191746.py deleted file mode 100644 index e951d32..0000000 --- a/.history/cmd_chat/server/services_20251105191746.py +++ /dev/null @@ -1,36 +0,0 @@ -import ast -from sanic import Websocket -from cmd_chat.server.models import Message - - -async def _get_bytes_and_serialize( - ws: Websocket -) -> dict: - return ast.literal_eval(await ws.recv()) - - -async def _check_ws_for_close_status( - response: dict, - ws: Websocket -) -> None: - if "action" in response.keys(): - if response["action"] == "close": - await ws.close() - - -async def _generate_new_message( - message: str -) -> Message: - return Message(message = message) - - -async def _generate_update_payload( - memory_msgs: list[Message], - users_structure: dict -) -> str: - return str({ - "messages": [i.message for i in memory_msgs], - "users_in_chat": list(users_structure.keys()) - }) - - diff --git a/.history/setup_20251105190626.py b/.history/setup_20251105190626.py deleted file mode 100644 index 7eef936..0000000 --- a/.history/setup_20251105190626.py +++ /dev/null @@ -1,33 +0,0 @@ -import setuptools - -with open("README.md", "r", encoding="utf-8") as fh: - description = fh.read() - -setuptools.setup( - name="secured_console_chat", - version="1.1.22", - author="dinosaurtirex", - author_email="sneakybeaky18@gmail.com", - # Use find_packages to correctly discover package names - packages=setuptools.find_packages(exclude=("tests", "docs")), - description="Secured console chat with RSA & Fernet", - long_description=description, - long_description_content_type="text/markdown", - url="https://github.com/dinosaurtirex/cmd-chat", - license='MIT', - python_requires='>=3.10', - entry_points={ - 'console_scripts': [ - 'cmd_chat = cmd_chat:main' - ] - }, - install_requires=[ - "sanic", - "requests", - "rsa", - "cryptography", - "colorama", - "pydantic", - "websocket-client" - ] -) \ No newline at end of file diff --git a/.history/setup_20251105190632.py b/.history/setup_20251105190632.py deleted file mode 100644 index 7eef936..0000000 --- a/.history/setup_20251105190632.py +++ /dev/null @@ -1,33 +0,0 @@ -import setuptools - -with open("README.md", "r", encoding="utf-8") as fh: - description = fh.read() - -setuptools.setup( - name="secured_console_chat", - version="1.1.22", - author="dinosaurtirex", - author_email="sneakybeaky18@gmail.com", - # Use find_packages to correctly discover package names - packages=setuptools.find_packages(exclude=("tests", "docs")), - description="Secured console chat with RSA & Fernet", - long_description=description, - long_description_content_type="text/markdown", - url="https://github.com/dinosaurtirex/cmd-chat", - license='MIT', - python_requires='>=3.10', - entry_points={ - 'console_scripts': [ - 'cmd_chat = cmd_chat:main' - ] - }, - install_requires=[ - "sanic", - "requests", - "rsa", - "cryptography", - "colorama", - "pydantic", - "websocket-client" - ] -) \ No newline at end of file