diff --git a/core/classes/modules/rehash.py b/core/classes/modules/rehash.py index aa842f8..6b96998 100644 --- a/core/classes/modules/rehash.py +++ b/core/classes/modules/rehash.py @@ -14,6 +14,7 @@ REHASH_MODULES = [ 'core.classes.modules.config', 'core.base', 'core.classes.modules.commands', + 'core.classes.modules.rpc', 'core.classes.interfaces.iprotocol', 'core.classes.interfaces.imodule', 'core.classes.protocols.command_handler', @@ -69,6 +70,8 @@ def restart_service(uplink: 'Irc', reason: str = "Restarting with no reason!") - def rehash_service(uplink: 'Irc', nickname: str) -> None: need_a_restart = ["SERVEUR_ID"] uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS) + uplink.Loader.RpcServer.stop_server() + restart_flag = False config_model_bakcup = uplink.Config mods = REHASH_MODULES @@ -80,7 +83,7 @@ def rehash_service(uplink: 'Irc', nickname: str) -> None: channel=uplink.Config.SERVICE_CHANLOG ) uplink.Utils = sys.modules['core.utils'] - uplink.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model + uplink.Config = uplink.Loader.Config = uplink.Loader.ConfModule.Configuration(uplink.Loader).configuration_model uplink.Config.HSID = config_model_bakcup.HSID uplink.Config.DEFENDER_INIT = config_model_bakcup.DEFENDER_INIT uplink.Config.DEFENDER_RESTART = config_model_bakcup.DEFENDER_RESTART @@ -113,6 +116,8 @@ def rehash_service(uplink: 'Irc', nickname: str) -> None: # Reload Main Commands Module uplink.Commands = uplink.Loader.CommandModule.Command(uplink.Loader) + uplink.Loader.RpcServer = uplink.Loader.RpcServerModule.JSONRPCServer(uplink.Loader) + uplink.Loader.RpcServer.start_server() uplink.Commands.DB_COMMANDS = uplink.Settings.get_cache('db_commands') uplink.Loader.Base = uplink.Loader.BaseModule.Base(uplink.Loader) diff --git a/core/classes/modules/rpc/rpc.py b/core/classes/modules/rpc/rpc.py new file mode 100644 index 0000000..95170a0 --- /dev/null +++ b/core/classes/modules/rpc/rpc.py @@ -0,0 +1,240 @@ +import base64 +import json +import logging +from enum import Enum +from http.server import BaseHTTPRequestHandler, HTTPServer +from typing import TYPE_CHECKING, Any, Optional +from core.classes.modules.rpc.rpc_user import RPCUser +from core.classes.modules.rpc.rpc_channel import RPCChannel +from core.classes.modules.rpc.rpc_command import RPCCommand + +if TYPE_CHECKING: + from core.loader import Loader + +ProxyLoader: Optional['Loader'] = None + +class RPCRequestHandler(BaseHTTPRequestHandler): + + def log_message(self, format, *args): + pass + + def do_POST(self): + logs = ProxyLoader.Logs + self.server_version = 'Defender6' + self.sys_version = ProxyLoader.Config.CURRENT_VERSION + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + request_data: dict = json.loads(body) + rip, rport = self.client_address + + if not self.authenticate(request_data): + return None + + response_data = { + 'jsonrpc': '2.0', + 'id': request_data.get('id', 123) + } + + method = request_data.get("method") + params: dict[str, Any] = request_data.get("params", {}) + response_data['method'] = method + http_code = 200 + + match method: + case 'user.list': + user = RPCUser(ProxyLoader) + response_data['result'] = user.user_list() + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del user + + case 'user.get': + user = RPCUser(ProxyLoader) + uid_or_nickname = params.get('uid_or_nickname', None) + response_data['result'] = user.user_get(uid_or_nickname) + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del user + + case 'channel.list': + channel = RPCChannel(ProxyLoader) + response_data['result'] = channel.channel_list() + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del channel + + case 'command.list': + command = RPCCommand(ProxyLoader) + response_data['result'] = command.command_list() + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del command + + case 'command.get.by.module': + command = RPCCommand(ProxyLoader) + module_name = params.get('name', None) + response_data['result'] = command.command_get_by_module(module_name) + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del command + + case 'command.get.by.name': + command = RPCCommand(ProxyLoader) + command_name = params.get('name', None) + response_data['result'] = command.command_get_by_name(command_name) + logs.debug(f'[RPC] {method} recieved from {rip}:{rport}') + del command + + case _: + response_data['error'] = create_error_response(JSONRPCErrorCode.METHOD_NOT_FOUND) + logs.debug(f'[RPC ERROR] {method} recieved from {rip}:{rport}') + http_code = 404 + + self.send_response(http_code) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(response_data).encode('utf-8')) + + return None + + def do_GET(self): + self.server_version = 'Defender6' + self.sys_version = ProxyLoader.Config.CURRENT_VERSION + content_length = int(self.headers['Content-Length']) + body = self.rfile.read(content_length) + request_data: dict = json.loads(body) + + if not self.authenticate(request_data): + return None + + response_data = {'jsonrpc': '2.0', 'id': request_data.get('id', 321), + 'error': create_error_response(JSONRPCErrorCode.INVALID_REQUEST)} + + self.send_response(404) + self.send_header('Content-Type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(response_data).encode('utf-8')) + + return None + + + def authenticate(self, request_data: dict) -> bool: + logs = ProxyLoader.Logs + auth = self.headers.get('Authorization', None) + if auth is None: + self.send_auth_error(request_data) + return False + + # Authorization header format: Basic base64(username:password) + auth_type, auth_string = auth.split(' ', 1) + if auth_type.lower() != 'basic': + self.send_auth_error(request_data) + return False + + try: + # Decode the base64-encoded username:password + decoded_credentials = base64.b64decode(auth_string).decode('utf-8') + username, password = decoded_credentials.split(":", 1) + + # Check the username and password. + for rpcuser in ProxyLoader.Irc.Config.RPC_USERS: + if rpcuser.get('USERNAME', None) == username and rpcuser.get('PASSWORD', None) == password: + return True + + self.send_auth_error(request_data) + return False + + except Exception as e: + self.send_auth_error(request_data) + logs.error(e) + return False + + def send_auth_error(self, request_data: dict) -> None: + + response_data = { + 'jsonrpc': '2.0', + 'id': request_data.get('id', 123), + 'error': create_error_response(JSONRPCErrorCode.AUTHENTICATION_ERROR) + } + + self.send_response(401) + self.send_header('WWW-Authenticate', 'Basic realm="Authorization Required"') + self.end_headers() + self.wfile.write(json.dumps(response_data).encode('utf-8')) + +class JSONRPCServer: + def __init__(self, loader: 'Loader'): + global ProxyLoader + + ProxyLoader = loader + self._Loader = loader + self._Base = loader.Base + self._Logs = loader.Logs + self.rpc_server: Optional[HTTPServer] = None + self.connected: bool = False + + def start_server(self, server_class=HTTPServer, handler_class=RPCRequestHandler, *, hostname: str = 'localhost', port: int = 5000): + logging.getLogger('http.server').setLevel(logging.CRITICAL) + server_address = (hostname, port) + self.rpc_server = server_class(server_address, handler_class) + self._Logs.debug(f"Server ready on http://{hostname}:{port}...") + self._Base.create_thread(self.thread_start_rpc_server, (), True) + + def thread_start_rpc_server(self) -> None: + self._Loader.Irc.Protocol.send_priv_msg( + self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has started successfuly!", self._Loader.Config.SERVICE_CHANLOG + ) + self.connected = True + self.rpc_server.serve_forever() + ProxyLoader.Logs.debug(f"RPC Server down!") + + def stop_server(self): + self._Base.create_thread(self.thread_stop_rpc_server) + + def thread_stop_rpc_server(self): + self.rpc_server.shutdown() + ProxyLoader.Logs.debug(f"RPC Server shutdown!") + self.rpc_server.server_close() + ProxyLoader.Logs.debug(f"RPC Server clean-up!") + self._Base.garbage_collector_thread() + self._Loader.Irc.Protocol.send_priv_msg( + self._Loader.Config.SERVICE_NICKNAME, "Defender RPC Server has stopped successfuly!", self._Loader.Config.SERVICE_CHANLOG + ) + self.connected = False + +class JSONRPCErrorCode(Enum): + PARSE_ERROR = -32700 # Syntax error in the request (malformed JSON) + INVALID_REQUEST = -32600 # Invalid Request (incorrect structure or missing fields) + METHOD_NOT_FOUND = -32601 # Method not found (the requested method does not exist) + INVALID_PARAMS = -32602 # Invalid Params (the parameters provided are incorrect) + INTERNAL_ERROR = -32603 # Internal Error (an internal server error occurred) + + # Custom application-specific errors (beyond standard JSON-RPC codes) + CUSTOM_ERROR = 1001 # Custom application-defined error (e.g., user not found) + AUTHENTICATION_ERROR = 1002 # Authentication failure (e.g., invalid credentials) + PERMISSION_ERROR = 1003 # Permission error (e.g., user does not have access to this method) + RESOURCE_NOT_FOUND = 1004 # Resource not found (e.g., the requested resource does not exist) + DUPLICATE_REQUEST = 1005 # Duplicate request (e.g., a similar request has already been processed) + + def description(self): + """Returns a description associated with each error code""" + descriptions = { + JSONRPCErrorCode.PARSE_ERROR: "The JSON request is malformed.", + JSONRPCErrorCode.INVALID_REQUEST: "The request is invalid (missing or incorrect fields).", + JSONRPCErrorCode.METHOD_NOT_FOUND: "The requested method could not be found.", + JSONRPCErrorCode.INVALID_PARAMS: "The parameters provided are invalid.", + JSONRPCErrorCode.INTERNAL_ERROR: "An internal error occurred on the server.", + JSONRPCErrorCode.CUSTOM_ERROR: "A custom error defined by the application.", + JSONRPCErrorCode.AUTHENTICATION_ERROR: "User authentication failed.", + JSONRPCErrorCode.PERMISSION_ERROR: "User does not have permission to access this method.", + JSONRPCErrorCode.RESOURCE_NOT_FOUND: "The requested resource could not be found.", + JSONRPCErrorCode.DUPLICATE_REQUEST: "The request is a duplicate or is already being processed.", + } + return descriptions.get(self, "Unknown error") + +def create_error_response(error_code: JSONRPCErrorCode, details: dict = None) -> dict[str, str]: + """Create a JSON-RPC error!""" + response = { + "code": error_code.value, + "message": error_code.description(), + } + + if details: + response["data"] = details + + return response \ No newline at end of file diff --git a/core/classes/modules/rpc/rpc_channel.py b/core/classes/modules/rpc/rpc_channel.py new file mode 100644 index 0000000..e45e782 --- /dev/null +++ b/core/classes/modules/rpc/rpc_channel.py @@ -0,0 +1,12 @@ +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from core.loader import Loader + +class RPCChannel: + def __init__(self, loader: 'Loader'): + self._Loader = loader + self._Channel = loader.Channel + + def channel_list(self) -> list[dict]: + return [chan.to_dict() for chan in self._Channel.UID_CHANNEL_DB] \ No newline at end of file diff --git a/core/classes/modules/rpc/rpc_command.py b/core/classes/modules/rpc/rpc_command.py new file mode 100644 index 0000000..1c2025d --- /dev/null +++ b/core/classes/modules/rpc/rpc_command.py @@ -0,0 +1,21 @@ +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from core.loader import Loader + +class RPCCommand: + def __init__(self, loader: 'Loader'): + self._Loader = loader + self._Command = loader.Commands + + def command_list(self) -> list[dict]: + return [command.to_dict() for command in self._Command.DB_COMMANDS] + + def command_get_by_module(self, module_name: str) -> list[dict]: + return [command.to_dict() for command in self._Command.DB_COMMANDS if command.module_name.lower() == module_name.lower()] + + def command_get_by_name(self, command_name: str) -> dict: + for command in self._Command.DB_COMMANDS: + if command.command_name.lower() == command_name.lower(): + return command.to_dict() + return {} \ No newline at end of file diff --git a/core/classes/modules/rpc/rpc_user.py b/core/classes/modules/rpc/rpc_user.py new file mode 100644 index 0000000..b9e014b --- /dev/null +++ b/core/classes/modules/rpc/rpc_user.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from core.loader import Loader + from core.definition import MUser + +class RPCUser: + def __init__(self, loader: 'Loader'): + self._Loader = loader + self._User = loader.User + + def user_list(self) -> list[dict]: + users = self._User.UID_DB.copy() + copy_users: list['MUser'] = [] + + for user in users: + copy_user = user.copy() + copy_user.connexion_datetime = copy_user.connexion_datetime.strftime('%d-%m-%Y') + copy_users.append(copy_user) + + return [user.to_dict() for user in copy_users] + + def user_get(self, uidornickname: str) -> Optional[dict]: + user = self._User.get_user(uidornickname) + if user: + user_copy = user.copy() + user_copy.connexion_datetime = user_copy.connexion_datetime.strftime('%d-%m-%Y') + return user_copy.to_dict() + + return None \ No newline at end of file diff --git a/core/definition.py b/core/definition.py index d7016e3..72f9062 100644 --- a/core/definition.py +++ b/core/definition.py @@ -1,6 +1,6 @@ from datetime import datetime from json import dumps -from dataclasses import dataclass, field, asdict, fields +from dataclasses import dataclass, field, asdict, fields, replace from typing import Literal, Any, Optional from os import sep @@ -14,6 +14,10 @@ class MainModel: def to_json(self) -> str: """Return the object of a dataclass a json str.""" return dumps(self.to_dict()) + + def copy(self): + """Return the object of a dataclass a json str.""" + return replace(self) def get_attributes(self) -> list[str]: """Return a list of attributes name""" @@ -205,6 +209,9 @@ class MConfig(MainModel): PASSWORD: str = "password" """The password of the admin of the service""" + RPC_USERS: list[dict] = field(default_factory=list) + """The Defender rpc users""" + JSONRPC_URL: str = None """The RPC url, if local https://127.0.0.1:PORT/api should be fine""" diff --git a/core/irc.py b/core/irc.py index 43aa828..18a8fbe 100644 --- a/core/irc.py +++ b/core/irc.py @@ -126,6 +126,8 @@ class Irc: self.build_command(4, 'core', 'rehash', 'Reload the configuration file without restarting') self.build_command(4, 'core', 'raw', 'Send a raw command directly to the IRC server') self.build_command(4, 'core', 'print_vars', 'Print users in a file.') + self.build_command(4, 'core', 'start_rpc', 'Start defender jsonrpc server') + self.build_command(4, 'core', 'stop_rpc', 'Stop defender jsonrpc server') # Define the IrcSocket object self.IrcSocket: Optional[Union[socket.socket, SSLSocket]] = None @@ -372,7 +374,7 @@ class Irc: return uptime - def heartbeat(self, beat:float) -> None: + def heartbeat(self, beat: float) -> None: """Execute certaines commandes de nettoyage toutes les x secondes x étant définit a l'initialisation de cette class (self.beat) @@ -1272,5 +1274,11 @@ class Irc: return None + case 'start_rpc': + self.Loader.RpcServer.start_server() + + case 'stop_rpc': + self.Loader.RpcServer.stop_server() + case _: pass diff --git a/core/loader.py b/core/loader.py index 8cdbfd8..c254552 100644 --- a/core/loader.py +++ b/core/loader.py @@ -8,6 +8,7 @@ import core.base as base_mod import core.module as module_mod import core.classes.modules.commands as commands_mod import core.classes.modules.config as conf_mod +import core.classes.modules.rpc.rpc as rpc_mod import core.irc as irc import core.classes.protocols.factory as factory @@ -26,6 +27,8 @@ class Loader: self.LoggingModule: logs = logs + self.RpcServerModule: rpc_mod = rpc_mod + self.Utils: utils = utils # Load Classes @@ -69,6 +72,8 @@ class Loader: self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self.Irc) + self.RpcServer: rpc_mod.JSONRPCServer = rpc_mod.JSONRPCServer(self) + self.Base.init() self.Logs.debug(self.Utils.tr("Loader %s success", __name__)) diff --git a/core/utils.py b/core/utils.py index e79752d..0abd02d 100644 --- a/core/utils.py +++ b/core/utils.py @@ -177,7 +177,7 @@ def generate_random_string(lenght: int) -> str: return randomize -def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> str: +def hash_password(password: str, algorithm: Literal["md5", "sha3_512"] = 'md5') -> str: """Return the crypted password following the selected algorithm Args: @@ -190,16 +190,16 @@ def hash_password(password: str, algorithm: Literal["md5, sha3_512"] = 'md5') -> match algorithm: case 'md5': - password = md5(password.encode()).hexdigest() - return password + hashed_password = md5(password.encode()).hexdigest() + return hashed_password case 'sha3_512': - password = sha3_512(password.encode()).hexdigest() - return password + hashed_password = sha3_512(password.encode()).hexdigest() + return hashed_password case _: - password = md5(password.encode()).hexdigest() - return password + hashed_password = md5(password.encode()).hexdigest() + return hashed_password def get_all_modules() -> list[str]: """Get list of all main modules diff --git a/defender.py b/defender.py index 6fc06f2..da46ad4 100644 --- a/defender.py +++ b/defender.py @@ -10,7 +10,7 @@ from core import install ############################################# try: - install.update_packages() + # install.update_packages() from core.loader import Loader loader = Loader() loader.Irc.init_irc() diff --git a/mods/defender/utils.py b/mods/defender/utils.py index d0be159..a930600 100644 --- a/mods/defender/utils.py +++ b/mods/defender/utils.py @@ -152,8 +152,13 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]): confmodel (ModConfModel): The Module Configuration """ p = uplink.Protocol - parser = p.parse_nick(srvmsg) - uid = uplink.Loader.Utils.clean_uid(parser.get('uid', None)) + u, new_nickname, timestamp = p.parse_nick(srvmsg) + + if u is None: + uplink.Logs.error(f"[USER OBJ ERROR {timestamp}] - {srvmsg}") + return None + + uid = u.uid confmodel = uplink.ModConfig get_reputation = uplink.Reputation.get_reputation(uid) @@ -166,7 +171,7 @@ def handle_on_nick(uplink: 'Defender', srvmsg: list[str]): # Update the new nickname oldnick = get_reputation.nickname - newnickname = parser.get('newnickname', None) + newnickname = new_nickname get_reputation.nickname = newnickname # If ban in all channel is ON then unban old nickname an ban the new nickname