From 719e705bab6faff4b373aecb2925a6e1f665a578 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Wed, 26 Jul 2023 05:00:49 +0300 Subject: [PATCH] Start adding RCON part! --- requirements.txt | 3 +- src/core/core.py | 3 + .../config_provider-builtins.pyi | 19 ++-- src/modules/ConfigProvider/config_provider.py | 6 +- src/modules/ConsoleSystem/RCON.py | 92 +++++++++++++++++++ .../ConsoleSystem/console_system-builtins.pyi | 38 ++++---- src/modules/ConsoleSystem/console_system.py | 4 + 7 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 src/modules/ConsoleSystem/RCON.py diff --git a/requirements.txt b/requirements.txt index 3696d5c..63755e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ pydantic~=2.0.2 click~=8.1.4 lupa~=2.0 toml~=0.10.2 -colorama~=0.4.6 \ No newline at end of file +colorama~=0.4.6 +cryptography~=41.0.2 \ No newline at end of file diff --git a/src/core/core.py b/src/core/core.py index cd6f69a..1cc0fb7 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -270,6 +270,9 @@ class Core: tasks = [] # self.udp.start, f_tasks = [self.tcp.start, self.udp._start, console.start, self.stop_me, self.heartbeat, self.check_alive] + if config.RCON['enabled']: + rcon = console.rcon(config.RCON['password'], config.RCON['server_ip'], config.RCON['server_port']) + f_tasks.append(rcon.start) for task in f_tasks: tasks.append(asyncio.create_task(task())) t = asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION) diff --git a/src/modules/ConfigProvider/config_provider-builtins.pyi b/src/modules/ConfigProvider/config_provider-builtins.pyi index ce92eee..ec7f07b 100644 --- a/src/modules/ConfigProvider/config_provider-builtins.pyi +++ b/src/modules/ConfigProvider/config_provider-builtins.pyi @@ -1,17 +1,14 @@ -import secrets +from typing import Dict class Config: - def __init__(self, auth=None, game=None, server=None, options=None, web=None): - self.Auth = auth or {"key": None, "private": True} - self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1} - self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", - "server_ip": "0.0.0.0", "server_port": 30814} - self.Options = options or {"language": "en", "encoding": "utf8", "speed_limit": 0, "use_queue": False, - "debug": False} - self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433, - "secret_key": secrets.token_hex(16)} - + Auth: Dict[str, object] + Game: Dict[str, object] + Server: Dict[str, object] + RCON: Dict[str, object] + Options: Dict[str, object] + WebAPI: Dict[str, object] + enc: str | None def __repr__(self): return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server) class config (Config): ... diff --git a/src/modules/ConfigProvider/config_provider.py b/src/modules/ConfigProvider/config_provider.py index 3978f41..9457a44 100644 --- a/src/modules/ConfigProvider/config_provider.py +++ b/src/modules/ConfigProvider/config_provider.py @@ -12,15 +12,17 @@ import yaml class Config: - def __init__(self, auth=None, game=None, server=None, options=None, web=None): + def __init__(self, auth=None, game=None, server=None, rcon=None, options=None, web=None): self.Auth = auth or {"key": None, "private": True} self.Game = game or {"map": "gridmap_v2", "players": 8, "max_cars": 1} self.Server = server or {"name": "KuiToi-Server", "description": "Welcome to KuiToi Server!", "server_ip": "0.0.0.0", "server_port": 30814} + self.RCON = rcon or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 10383, + "password": secrets.token_hex(16)} self.Options = options or {"language": "en", "encoding": "utf-8", "speed_limit": 0, "use_queue": False, "debug": False, "use_lua": False, "log_chat": True} self.WebAPI = web or {"enabled": False, "server_ip": "127.0.0.1", "server_port": 8433, - "secret_key": secrets.token_hex(16)} + "access_token": secrets.token_hex(16)} def __repr__(self): return "%s(Auth=%r, Game=%r, Server=%r)" % (self.__class__.__name__, self.Auth, self.Game, self.Server) diff --git a/src/modules/ConsoleSystem/RCON.py b/src/modules/ConsoleSystem/RCON.py new file mode 100644 index 0000000..fb8b697 --- /dev/null +++ b/src/modules/ConsoleSystem/RCON.py @@ -0,0 +1,92 @@ +import hashlib +import os +from base64 import b64decode, b64encode + +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from core import get_logger + +""" +shared key: SHA256 of "password" +
: "\x00\x00\x00\x00" (Byte order: Little Endian) - like you use +: A set of random bytes packed in base64 (New for each message) +-> To server +<- From server + +Open TCP connection / +| -> ":hello" Without header, immediately with AES encryption (shared key) +| *Decrypt and some processes* +| Fail / +| | <- ":E:Bad key" | ":E:Error Message" Without header, without AES encryption +| | tcp.close() # End +| Success / +| | <- ":hello" with header, with AES encryption +| | (Next, everywhere with header, with AES encryption) +| -> ":
Cs:ver" +| <- ":
Os:KuiToi 0.4.3 | ":
Os:BeamMP 3.2.0" +| # Prints server and they version +| -> ":
Cs:commands" +| <- ":
Os:stop,help,plugins" | ":
Os:SKIP" For an autocomplete; "SKIP" For no autocomplete; +| *Ready to handle commands* +| -> ":
C:help" +| <- ":
O:stop: very cool stop\nhelp: Yayayayoy" +| -> ":
C:...." +| <- ":
O:...." +| -> ":
C:exit" +| tcp.close() + +Codes: +* "hello" - Hello message +* "E:error_message" - Send RCON error +* "C:command" - Receive command +* "Cs:" - Receive system command +* "O:output" - Send command output +* "Os:" - Send system output + +""" + + +class RCONSystem: + console = None + + def __init__(self, key, host, port): + self.log = get_logger("RCON") + self.key = key + self.host = host + self.port = port + + def encrypt(self, message, key): + self.log.debug(f"Encrypt message: {message}") + key = hashlib.sha256(key).digest() + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) + encryptor = cipher.encryptor() + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_data = padder.update(message.encode('utf-8')) + padder.finalize() + encrypted_data = encryptor.update(padded_data) + encryptor.finalize() + encoded_data = b64encode(encrypted_data) + encoded_iv = b64encode(iv) + return encoded_iv + b":" + encoded_data + + def decrypt(self, ciphertext, key): + self.log.debug(f"Dencrypt message: {ciphertext}") + key = hashlib.sha256(key).digest() + encoded_iv, encoded_data = ciphertext.split(":") + iv = b64decode(encoded_iv) + encrypted_data = b64decode(encoded_data) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) + decryptor = cipher.decryptor() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize() + unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize() + return unpadded_data.decode('utf-8') + + async def handle_client(self): + pass + + async def start(self): + self.log.info("TODO: RCON") + + async def stop(self): + pass diff --git a/src/modules/ConsoleSystem/console_system-builtins.pyi b/src/modules/ConsoleSystem/console_system-builtins.pyi index 691e70b..6238aea 100644 --- a/src/modules/ConsoleSystem/console_system-builtins.pyi +++ b/src/modules/ConsoleSystem/console_system-builtins.pyi @@ -1,36 +1,34 @@ -class Console(object): +from logging import Logger +from typing import AnyStr - def __init__(self, - prompt_in: str = ">", - prompt_out: str = "]:", - not_found: str = "Command \"%s\" not found in alias.") -> None: ... +from core import get_logger - def __getitem__(self, item): ... - @property - def alias(self) -> dict: ... - def add(self, key: str, func: function) -> dict: ... - def log(self, s: str, r='\r') -> None: ... - def write(self, s: str, r='\r') -> None: ... - def __lshift__(self, s: AnyStr) -> None: ... - def logger_hook(self) -> None: ... - def builtins_hook(self) -> None: ... - async def start(self) -> None: ... -class console(object): +class RCONSystem: + console = None + + def __init__(self, key, host, port): + self.log = get_logger("RCON") + self.key = key + self.host = host + self.port = port + + async def start(self): ... + async def stop(self): ... + +class console: + rcon: RCONSystem = RCONSystem @staticmethod def alias() -> dict: ... @staticmethod - def add_command(key: str, func: function) -> dict: ... - + def add_command(key: str, func, man: str = None, desc: str = None, custom_completer: dict = None) -> dict: ... @staticmethod async def start() -> None: ... - @staticmethod def builtins_hook() -> None: ... @staticmethod def logger_hook() -> None: ... - @staticmethod def log(s: str) -> None: ... @staticmethod diff --git a/src/modules/ConsoleSystem/console_system.py b/src/modules/ConsoleSystem/console_system.py index 8ac65b1..fb33108 100644 --- a/src/modules/ConsoleSystem/console_system.py +++ b/src/modules/ConsoleSystem/console_system.py @@ -18,6 +18,7 @@ from prompt_toolkit.output.win32 import NoConsoleScreenBufferError from prompt_toolkit.patch_stdout import patch_stdout from core import get_logger +from modules.ConsoleSystem import RCON class Console: @@ -45,6 +46,9 @@ class Console: self.add_command("help", self.__create_help_message, i18n.man_message_help, i18n.help_message_help, custom_completer={"help": {"--raw": None}}) self.completer = NestedCompleter.from_nested_dict(self.__alias) + rcon = RCON + rcon.console = self + self.rcon = rcon def __debug(self, *x): self.__logger.debug(f"{x}")