From 9e688f796423259c5e16ad383f1f3312d1a39dae Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:53:03 +0100 Subject: [PATCH 1/7] error 204 crash the RPC server. changing the 204 error to 404 when data not found --- core/classes/interfaces/irpc_endpoint.py | 2 ++ core/classes/modules/rpc/rpc.py | 5 +++-- core/classes/modules/rpc/rpc_channel.py | 1 - core/classes/modules/rpc/rpc_user.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/classes/interfaces/irpc_endpoint.py b/core/classes/interfaces/irpc_endpoint.py index 0f5d0b4..b1612ad 100644 --- a/core/classes/interfaces/irpc_endpoint.py +++ b/core/classes/interfaces/irpc_endpoint.py @@ -12,12 +12,14 @@ class IRPC: self.http_status_code = http_status_code self.response_model = { "jsonrpc": "2.0", + "method": 'unknown', "id": 123 } def reset(self): self.response_model = { "jsonrpc": "2.0", + "method": 'unknown', "id": 123 } diff --git a/core/classes/modules/rpc/rpc.py b/core/classes/modules/rpc/rpc.py index 1b6646d..39b7050 100644 --- a/core/classes/modules/rpc/rpc.py +++ b/core/classes/modules/rpc/rpc.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: class JSonRpcServer: - def __init__(self, context: 'Loader', *, hostname: str = 'localhost', port: int = 5000): + def __init__(self, context: 'Loader', *, hostname: str = '0.0.0.0', port: int = 5000): self._ctx = context self.live: bool = False self.host = hostname @@ -77,10 +77,10 @@ class JSonRpcServer: response_data = { "jsonrpc": "2.0", + "method": method, "id": request_data.get('id', 123) } - response_data['method'] = method rip = request.client.host rport = request.client.port http_code = http_status_code.HTTP_200_OK @@ -89,6 +89,7 @@ class JSonRpcServer: r: JSONResponse = self.methods[method](**params) resp = json.loads(r.body) resp['id'] = request_data.get('id', 123) + resp['method'] = method return JSONResponse(resp, r.status_code) response_data['error'] = rpcerr.create_error_response(rpcerr.JSONRPCErrorCode.METHOD_NOT_FOUND) diff --git a/core/classes/modules/rpc/rpc_channel.py b/core/classes/modules/rpc/rpc_channel.py index b1388eb..74841f1 100644 --- a/core/classes/modules/rpc/rpc_channel.py +++ b/core/classes/modules/rpc/rpc_channel.py @@ -1,5 +1,4 @@ from typing import TYPE_CHECKING - from starlette.responses import JSONResponse from core.classes.interfaces.irpc_endpoint import IRPC from core.classes.modules.rpc.rpc_errors import JSONRPCErrorCode diff --git a/core/classes/modules/rpc/rpc_user.py b/core/classes/modules/rpc/rpc_user.py index 667005a..7bb3c3a 100644 --- a/core/classes/modules/rpc/rpc_user.py +++ b/core/classes/modules/rpc/rpc_user.py @@ -42,4 +42,4 @@ class RPCUser(IRPC): return JSONResponse(self.response_model) self.response_model['result'] = 'User not found!' - return JSONResponse(self.response_model, self.http_status_code.HTTP_204_NO_CONTENT) \ No newline at end of file + return JSONResponse(self.response_model, self.http_status_code.HTTP_404_NOT_FOUND) \ No newline at end of file From d66d297a338bbcd5f4729ac2ffff2c92c7d8f7a0 Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:12:58 +0100 Subject: [PATCH 2/7] Fix await issue by Adding await to asyncio.sleep. replace build_command location --- mods/votekick/mod_votekick.py | 2 +- mods/votekick/threads.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mods/votekick/mod_votekick.py b/mods/votekick/mod_votekick.py index 86aa308..b605f13 100644 --- a/mods/votekick/mod_votekick.py +++ b/mods/votekick/mod_votekick.py @@ -88,7 +88,7 @@ class Votekick(IModule): self.VoteKickManager.VOTE_CHANNEL_DB = metadata # Créer les nouvelles commandes du module - self.ctx.Irc.build_command(1, self.module_name, 'vote', 'The kick vote module') + self.ctx.Commands.build_command(1, self.module_name, 'vote', 'The kick vote module') async def unload(self) -> None: try: diff --git a/mods/votekick/threads.py b/mods/votekick/threads.py index 14eb209..44e14ee 100644 --- a/mods/votekick/threads.py +++ b/mods/votekick/threads.py @@ -12,7 +12,7 @@ async def timer_vote_verdict(uplink: 'Votekick', channel: str) -> None: if not uplink.VoteKickManager.is_vote_ongoing(channel): return None - asyncio.sleep(60) + await asyncio.sleep(60) votec = uplink.VoteKickManager.get_vote_channel_model(channel) if votec: From 226340e1aa5349b6bd6cb02b7b69b8a60092e4a0 Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 16:15:10 +0100 Subject: [PATCH 3/7] Replace build_command location, code refactoring on irc --- core/irc.py | 216 +++++++++++++++--------------------- mods/clone/mod_clone.py | 2 +- mods/command/mod_command.py | 102 ++++++++--------- mods/jsonrpc/mod_jsonrpc.py | 7 +- mods/test/mod_test.py | 10 +- 5 files changed, 149 insertions(+), 188 deletions(-) diff --git a/core/irc.py b/core/irc.py index c691237..8d4a980 100644 --- a/core/irc.py +++ b/core/irc.py @@ -1,8 +1,7 @@ import asyncio -import socket import re -import time -from ssl import SSLSocket +import ssl +import threading from datetime import datetime, timedelta from typing import TYPE_CHECKING, Any, Optional, Union from core.classes.modules import rehash @@ -30,6 +29,10 @@ class Irc: # Load Context class (Loader) self.ctx = loader + # Define Reader and Writer + self.reader: Optional[asyncio.StreamReader] = None + self.writer: Optional[asyncio.StreamWriter] = None + # Date et heure de la premiere connexion de Defender self.defender_connexion_datetime = self.ctx.Config.DEFENDER_CONNEXION_DATETIME @@ -58,50 +61,56 @@ class Irc: # Load Commands Utils # self.Commands = self.Loader.Commands """Command utils""" + self.ctx.Commands.build_command(0, 'core', 'help', 'This provide the help') + self.ctx.Commands.build_command(0, 'core', 'auth', 'Login to the IRC Service') + self.ctx.Commands.build_command(0, 'core', 'copyright', 'Give some information about the IRC Service') + self.ctx.Commands.build_command(0, 'core', 'uptime', 'Give you since when the service is connected') + self.ctx.Commands.build_command(0, 'core', 'firstauth', 'First authentication of the Service') + self.ctx.Commands.build_command(0, 'core', 'register', f'Register your nickname /msg {self.ctx.Config.SERVICE_NICKNAME} REGISTER ') + self.ctx.Commands.build_command(0, 'core', 'identify', f'Identify yourself with your password /msg {self.ctx.Config.SERVICE_NICKNAME} IDENTIFY ') + self.ctx.Commands.build_command(0, 'core', 'logout', 'Reverse the effect of the identify command') + self.ctx.Commands.build_command(1, 'core', 'load', 'Load an existing module') + self.ctx.Commands.build_command(1, 'core', 'unload', 'Unload a module') + self.ctx.Commands.build_command(1, 'core', 'reload', 'Reload a module') + self.ctx.Commands.build_command(1, 'core', 'deauth', 'Deauth from the irc service') + self.ctx.Commands.build_command(1, 'core', 'checkversion', 'Check the version of the irc service') + self.ctx.Commands.build_command(2, 'core', 'show_modules', 'Display a list of loaded modules') + self.ctx.Commands.build_command(2, 'core', 'show_timers', 'Display active timers') + self.ctx.Commands.build_command(2, 'core', 'show_threads', 'Display active threads in the system') + self.ctx.Commands.build_command(2, 'core', 'show_asyncio', 'Display active asyncio') + self.ctx.Commands.build_command(2, 'core', 'show_channels', 'Display a list of active channels') + self.ctx.Commands.build_command(2, 'core', 'show_users', 'Display a list of connected users') + self.ctx.Commands.build_command(2, 'core', 'show_clients', 'Display a list of connected clients') + self.ctx.Commands.build_command(2, 'core', 'show_admins', 'Display a list of administrators') + self.ctx.Commands.build_command(2, 'core', 'show_configuration', 'Display the current configuration settings') + self.ctx.Commands.build_command(2, 'core', 'show_cache', 'Display the current cache') + self.ctx.Commands.build_command(2, 'core', 'clear_cache', 'Clear the cache!') + self.ctx.Commands.build_command(3, 'core', 'quit', 'Disconnect the bot or user from the server.') + self.ctx.Commands.build_command(3, 'core', 'restart', 'Restart the bot or service.') + self.ctx.Commands.build_command(3, 'core', 'addaccess', 'Add a user or entity to an access list with specific permissions.') + self.ctx.Commands.build_command(3, 'core', 'editaccess', 'Modify permissions for an existing user or entity in the access list.') + self.ctx.Commands.build_command(3, 'core', 'delaccess', 'Remove a user or entity from the access list.') + self.ctx.Commands.build_command(3, 'core', 'cert', 'Append your new fingerprint to your account!') + self.ctx.Commands.build_command(4, 'core', 'rehash', 'Reload the configuration file without restarting') + self.ctx.Commands.build_command(4, 'core', 'raw', 'Send a raw command directly to the IRC server') + self.ctx.Commands.build_command(4, 'core', 'print_vars', 'Print users in a file.') + self.ctx.Commands.build_command(4, 'core', 'start_rpc', 'Start defender jsonrpc server') + self.ctx.Commands.build_command(4, 'core', 'stop_rpc', 'Stop defender jsonrpc server') - self.build_command(0, 'core', 'help', 'This provide the help') - self.build_command(0, 'core', 'auth', 'Login to the IRC Service') - self.build_command(0, 'core', 'copyright', 'Give some information about the IRC Service') - self.build_command(0, 'core', 'uptime', 'Give you since when the service is connected') - self.build_command(0, 'core', 'firstauth', 'First authentication of the Service') - self.build_command(0, 'core', 'register', f'Register your nickname /msg {self.ctx.Config.SERVICE_NICKNAME} REGISTER ') - self.build_command(0, 'core', 'identify', f'Identify yourself with your password /msg {self.ctx.Config.SERVICE_NICKNAME} IDENTIFY ') - self.build_command(0, 'core', 'logout', 'Reverse the effect of the identify command') - self.build_command(1, 'core', 'load', 'Load an existing module') - self.build_command(1, 'core', 'unload', 'Unload a module') - self.build_command(1, 'core', 'reload', 'Reload a module') - self.build_command(1, 'core', 'deauth', 'Deauth from the irc service') - self.build_command(1, 'core', 'checkversion', 'Check the version of the irc service') - self.build_command(2, 'core', 'show_modules', 'Display a list of loaded modules') - self.build_command(2, 'core', 'show_timers', 'Display active timers') - self.build_command(2, 'core', 'show_threads', 'Display active threads in the system') - self.build_command(2, 'core', 'show_asyncio', 'Display active asyncio') - self.build_command(2, 'core', 'show_channels', 'Display a list of active channels') - self.build_command(2, 'core', 'show_users', 'Display a list of connected users') - self.build_command(2, 'core', 'show_clients', 'Display a list of connected clients') - self.build_command(2, 'core', 'show_admins', 'Display a list of administrators') - self.build_command(2, 'core', 'show_configuration', 'Display the current configuration settings') - self.build_command(2, 'core', 'show_cache', 'Display the current cache') - self.build_command(2, 'core', 'clear_cache', 'Clear the cache!') - self.build_command(3, 'core', 'quit', 'Disconnect the bot or user from the server.') - self.build_command(3, 'core', 'restart', 'Restart the bot or service.') - self.build_command(3, 'core', 'addaccess', 'Add a user or entity to an access list with specific permissions.') - self.build_command(3, 'core', 'editaccess', 'Modify permissions for an existing user or entity in the access list.') - self.build_command(3, 'core', 'delaccess', 'Remove a user or entity from the access list.') - self.build_command(3, 'core', 'cert', 'Append your new fingerprint to your account!') - 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') + ############################################## + # CONNEXION IRC # + ############################################## - # Define the IrcSocket object - self.IrcSocket: Optional[Union[socket.socket, SSLSocket]] = None - - self.reader: Optional[asyncio.StreamReader] = None - self.writer: Optional[asyncio.StreamWriter] = None - - self.ctx.Base.create_asynctask(self.heartbeat(self.beat)) + async def run(self): + try: + await self.connect() + await self.listen() + except asyncio.exceptions.IncompleteReadError as ie: + # When IRCd server is down + # asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of undefined expected bytes + self.ctx.Logs.critical(f"The IRCd server is no more connected! {ie}") + except asyncio.exceptions.CancelledError as cerr: + self.ctx.Logs.debug(f"Asyncio CancelledError reached! {cerr}") async def connect(self): @@ -116,24 +125,32 @@ class Irc: await self.Protocol.send_link() async def listen(self): + self.ctx.Base.create_asynctask( + self.ctx.Base.create_thread_io(self.ctx.Utils.heartbeat, True, self.ctx, self.beat) + ) + while self.signal: data = await self.reader.readuntil(b'\r\n') await self.send_response(data.splitlines()) - async def run(self): + async def send_response(self, responses:list[bytes]) -> None: try: - await self.connect() - await self.listen() - except asyncio.exceptions.IncompleteReadError as ie: - # When IRCd server is down - # asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of undefined expected bytes - self.ctx.Logs.critical(f"The IRCd server is no more connected! {ie}") - except asyncio.exceptions.CancelledError as cerr: - self.ctx.Logs.debug(f"Asyncio CancelledError reached! {cerr}") + for data in responses: + response = data.decode(self.CHARSET[0]).split() + await self.cmd(response) - ############################################## - # CONNEXION IRC # - ############################################## + except (UnicodeEncodeError, UnicodeDecodeError) as ue: + for data in responses: + response = data.decode(self.CHARSET[1], 'replace').split() + await self.cmd(response) + self.ctx.Logs.error(f'UnicodeEncodeError: {ue}') + self.ctx.Logs.error(responses) + except AssertionError as ae: + self.ctx.Logs.error(f"Assertion error : {ae}") + + # -------------------------------------------- + # FIN CONNEXION IRC # + # -------------------------------------------- def init_service_user(self) -> None: @@ -157,51 +174,6 @@ class Irc: chan = chan_name[0] await self.Protocol.send_sjoin(channel=chan) - async def send_response(self, responses:list[bytes]) -> None: - try: - for data in responses: - response = data.decode(self.CHARSET[0]).split() - await self.cmd(response) - - except UnicodeEncodeError as ue: - for data in responses: - response = data.decode(self.CHARSET[1],'replace').split() - await self.cmd(response) - self.ctx.Logs.error(f'UnicodeEncodeError: {ue}') - self.ctx.Logs.error(response) - - except UnicodeDecodeError as ud: - for data in responses: - response = data.decode(self.CHARSET[1],'replace').split() - await self.cmd(response) - self.ctx.Logs.error(f'UnicodeDecodeError: {ud}') - self.ctx.Logs.error(response) - - except AssertionError as ae: - self.ctx.Logs.error(f"Assertion error : {ae}") - - def unload(self) -> None: - # This is only to reference the method - return None - - # -------------------------------------------- - # FIN CONNEXION IRC # - # -------------------------------------------- - - def build_command(self, level: int, module_name: str, command_name: str, command_description: str) -> None: - """This method build the commands variable - - Args: - level (int): The Level of the command - module_name (str): The module name - command_name (str): The command name - command_description (str): The description of the command - """ - # Build Model. - self.ctx.Commands.build(self.ctx.Definition.MCommand(module_name, command_name, command_description, level)) - - return None - async def generate_help_menu(self, nickname: str, module: Optional[str] = None) -> None: # Check if the nickname is an admin @@ -289,17 +261,6 @@ class Irc: return uptime - async 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) - - Args: - beat (float): Nombre de secondes entre chaque exécution - """ - while self.hb_active: - await asyncio.sleep(beat) - self.ctx.Base.execute_periodic_action() - def insert_db_admin(self, uid: str, account: str, level: int, language: str) -> None: user_obj = self.ctx.User.get_user(uid) @@ -413,12 +374,6 @@ class Irc: for module in modules: await module.class_instance.cmd(original_response) if self.ctx.Utils.is_coroutinefunction(module.class_instance.cmd) else module.class_instance.cmd(original_response) - # if len(original_response) > 2: - # if original_response[2] != 'UID': - # # Envoyer la commande aux classes dynamiquement chargées - # for module in self.ctx.ModuleUtils.model_get_loaded_modules().copy(): - # module.class_instance.cmd(original_response) - except IndexError as ie: self.ctx.Logs.error(f"IndexError: {ie}") except Exception as err: @@ -441,7 +396,7 @@ class Irc: if u is None: return None - c = self.ctx.Client.get_Client(u.uid) + c = self.ctx.Client.get_client(u.uid) """The Client Object""" fromuser = u.nickname @@ -627,7 +582,7 @@ class Irc: level = self.ctx.Base.int_if_possible(cmd[2]) password = str(cmd[3]) - self.create_defender_user(fromuser, new_admin, level, password) + await self.create_defender_user(fromuser, new_admin, level, password) return None except IndexError as ie: @@ -982,13 +937,12 @@ class Irc: try: final_reason = ' '.join(cmd[1:]) self.hb_active = False - await self.ctx.Base.shutdown() + await rehash.shutdown(self.ctx) self.ctx.Base.execute_periodic_action() for chan_name in self.ctx.Channel.UID_CHANNEL_DB: - # self.Protocol.send_mode_chan(chan_name.name, '-l') await self.Protocol.send_set_mode('-l', channel_name=chan_name.name) - + for client in self.ctx.Client.CLIENT_DB: await self.Protocol.send_svslogout(client) @@ -1002,7 +956,6 @@ class Irc: self.ctx.Logs.info(f'Arrêt du server {dnickname}') self.ctx.Config.DEFENDER_RESTART = 0 - await self.writer.drain() self.writer.close() await self.writer.wait_closed() @@ -1011,6 +964,8 @@ class Irc: except ConnectionResetError: if self.writer.is_closing(): self.ctx.Logs.debug(f"Defender stopped properly!") + except ssl.SSLError as serr: + self.ctx.Logs.error(f"Defender has ended with an SSL Error! - {serr}") case 'restart': final_reason = ' '.join(cmd[1:]) @@ -1079,6 +1034,13 @@ class Irc: nick_to=fromuser, msg=f">> {thread.name} ({thread.is_alive()})" ) + + for thread in threading.enumerate(): + await self.Protocol.send_notice( + nick_from=dnickname, + nick_to=fromuser, + msg=f">> Thread name: {thread.name} - Is alive: {thread.is_alive()} - Daemon: {thread.daemon}" + ) return None @@ -1209,10 +1171,10 @@ class Irc: return None case 'start_rpc': - self.ctx.Base.create_asynctask(self.ctx.RpcServer.start_server()) + self.ctx.Base.create_asynctask(self.ctx.RpcServer.start_rpc_server()) case 'stop_rpc': - self.ctx.Base.create_asynctask(self.ctx.RpcServer.stop_server()) + self.ctx.Base.create_asynctask(self.ctx.RpcServer.stop_rpc_server()) case _: pass diff --git a/mods/clone/mod_clone.py b/mods/clone/mod_clone.py index 6ae6084..7001828 100644 --- a/mods/clone/mod_clone.py +++ b/mods/clone/mod_clone.py @@ -73,7 +73,7 @@ class Clone(IModule): self.ctx.Logs.debug(f"Cache Size = {self.ctx.Settings.get_cache_size()}") # Créer les nouvelles commandes du module - self.ctx.Irc.build_command(1, self.module_name, 'clone', 'Connect, join, part, kill and say clones') + self.ctx.Commands.build_command(1, self.module_name, 'clone', 'Connect, join, part, kill and say clones') await self.ctx.Channel.db_query_channel(action='add', module_name=self.module_name, channel_name=self.ctx.Config.CLONE_CHANNEL) await self.ctx.Irc.Protocol.send_sjoin(self.ctx.Config.CLONE_CHANNEL) diff --git a/mods/command/mod_command.py b/mods/command/mod_command.py index f7a7aaa..1146703 100644 --- a/mods/command/mod_command.py +++ b/mods/command/mod_command.py @@ -65,56 +65,56 @@ class Command(IModule): for c in new_cmds: self.ctx.Irc.Protocol.known_protocol.add(c) - self.ctx.Irc.build_command(2, self.module_name, 'join', 'Join a channel') - self.ctx.Irc.build_command(2, self.module_name, 'assign', 'Assign a user to a role or task') - self.ctx.Irc.build_command(2, self.module_name, 'part', 'Leave a channel') - self.ctx.Irc.build_command(2, self.module_name, 'unassign', 'Remove a user from a role or task') - self.ctx.Irc.build_command(2, self.module_name, 'owner', 'Give channel ownership to a user') - self.ctx.Irc.build_command(2, self.module_name, 'deowner', 'Remove channel ownership from a user') - self.ctx.Irc.build_command(2, self.module_name, 'protect', 'Protect a user from being kicked') - self.ctx.Irc.build_command(2, self.module_name, 'deprotect', 'Remove protection from a user') - self.ctx.Irc.build_command(2, self.module_name, 'op', 'Grant operator privileges to a user') - self.ctx.Irc.build_command(2, self.module_name, 'deop', 'Remove operator privileges from a user') - self.ctx.Irc.build_command(1, self.module_name, 'halfop', 'Grant half-operator privileges to a user') - self.ctx.Irc.build_command(1, self.module_name, 'dehalfop', 'Remove half-operator privileges from a user') - self.ctx.Irc.build_command(1, self.module_name, 'voice', 'Grant voice privileges to a user') - self.ctx.Irc.build_command(1, self.module_name, 'devoice', 'Remove voice privileges from a user') - self.ctx.Irc.build_command(1, self.module_name, 'topic', 'Change the topic of a channel') - self.ctx.Irc.build_command(2, self.module_name, 'opall', 'Grant operator privileges to all users') - self.ctx.Irc.build_command(2, self.module_name, 'deopall', 'Remove operator privileges from all users') - self.ctx.Irc.build_command(2, self.module_name, 'devoiceall', 'Remove voice privileges from all users') - self.ctx.Irc.build_command(2, self.module_name, 'voiceall', 'Grant voice privileges to all users') - self.ctx.Irc.build_command(2, self.module_name, 'ban', 'Ban a user from a channel') - self.ctx.Irc.build_command(2, self.module_name, 'automode', 'Automatically set user modes upon join') - self.ctx.Irc.build_command(2, self.module_name, 'unban', 'Remove a ban from a user') - self.ctx.Irc.build_command(2, self.module_name, 'kick', 'Kick a user from a channel') - self.ctx.Irc.build_command(2, self.module_name, 'kickban', 'Kick and ban a user from a channel') - self.ctx.Irc.build_command(2, self.module_name, 'umode', 'Set user mode') - self.ctx.Irc.build_command(2, self.module_name, 'mode', 'Set channel mode') - self.ctx.Irc.build_command(2, self.module_name, 'get_mode', 'Retrieve current channel mode') - self.ctx.Irc.build_command(2, self.module_name, 'svsjoin', 'Force a user to join a channel') - self.ctx.Irc.build_command(2, self.module_name, 'svspart', 'Force a user to leave a channel') - self.ctx.Irc.build_command(2, self.module_name, 'svsnick', 'Force a user to change their nickname') - self.ctx.Irc.build_command(2, self.module_name, 'wallops', 'Send a message to all operators') - self.ctx.Irc.build_command(2, self.module_name, 'globops', 'Send a global operator message') - self.ctx.Irc.build_command(2, self.module_name, 'gnotice', 'Send a global notice') - self.ctx.Irc.build_command(2, self.module_name, 'whois', 'Get information about a user') - self.ctx.Irc.build_command(2, self.module_name, 'names', 'List users in a channel') - self.ctx.Irc.build_command(2, self.module_name, 'invite', 'Invite a user to a channel') - self.ctx.Irc.build_command(2, self.module_name, 'inviteme', 'Invite yourself to a channel') - self.ctx.Irc.build_command(2, self.module_name, 'sajoin', 'Force yourself into a channel') - self.ctx.Irc.build_command(2, self.module_name, 'sapart', 'Force yourself to leave a channel') - self.ctx.Irc.build_command(2, self.module_name, 'kill', 'Disconnect a user from the server') - self.ctx.Irc.build_command(2, self.module_name, 'gline', 'Ban a user from the entire server') - self.ctx.Irc.build_command(2, self.module_name, 'ungline', 'Remove a global server ban') - self.ctx.Irc.build_command(2, self.module_name, 'kline', 'Ban a user based on their hostname') - self.ctx.Irc.build_command(2, self.module_name, 'unkline', 'Remove a K-line ban') - self.ctx.Irc.build_command(2, self.module_name, 'shun', 'Prevent a user from sending messages') - self.ctx.Irc.build_command(2, self.module_name, 'unshun', 'Remove a shun from a user') - self.ctx.Irc.build_command(2, self.module_name, 'glinelist', 'List all global bans') - self.ctx.Irc.build_command(2, self.module_name, 'shunlist', 'List all shunned users') - self.ctx.Irc.build_command(2, self.module_name, 'klinelist', 'List all K-line bans') - self.ctx.Irc.build_command(3, self.module_name, 'map', 'Show the server network map') + self.ctx.Commands.build_command(2, self.module_name, 'join', 'Join a channel') + self.ctx.Commands.build_command(2, self.module_name, 'assign', 'Assign a user to a role or task') + self.ctx.Commands.build_command(2, self.module_name, 'part', 'Leave a channel') + self.ctx.Commands.build_command(2, self.module_name, 'unassign', 'Remove a user from a role or task') + self.ctx.Commands.build_command(2, self.module_name, 'owner', 'Give channel ownership to a user') + self.ctx.Commands.build_command(2, self.module_name, 'deowner', 'Remove channel ownership from a user') + self.ctx.Commands.build_command(2, self.module_name, 'protect', 'Protect a user from being kicked') + self.ctx.Commands.build_command(2, self.module_name, 'deprotect', 'Remove protection from a user') + self.ctx.Commands.build_command(2, self.module_name, 'op', 'Grant operator privileges to a user') + self.ctx.Commands.build_command(2, self.module_name, 'deop', 'Remove operator privileges from a user') + self.ctx.Commands.build_command(1, self.module_name, 'halfop', 'Grant half-operator privileges to a user') + self.ctx.Commands.build_command(1, self.module_name, 'dehalfop', 'Remove half-operator privileges from a user') + self.ctx.Commands.build_command(1, self.module_name, 'voice', 'Grant voice privileges to a user') + self.ctx.Commands.build_command(1, self.module_name, 'devoice', 'Remove voice privileges from a user') + self.ctx.Commands.build_command(1, self.module_name, 'topic', 'Change the topic of a channel') + self.ctx.Commands.build_command(2, self.module_name, 'opall', 'Grant operator privileges to all users') + self.ctx.Commands.build_command(2, self.module_name, 'deopall', 'Remove operator privileges from all users') + self.ctx.Commands.build_command(2, self.module_name, 'devoiceall', 'Remove voice privileges from all users') + self.ctx.Commands.build_command(2, self.module_name, 'voiceall', 'Grant voice privileges to all users') + self.ctx.Commands.build_command(2, self.module_name, 'ban', 'Ban a user from a channel') + self.ctx.Commands.build_command(2, self.module_name, 'automode', 'Automatically set user modes upon join') + self.ctx.Commands.build_command(2, self.module_name, 'unban', 'Remove a ban from a user') + self.ctx.Commands.build_command(2, self.module_name, 'kick', 'Kick a user from a channel') + self.ctx.Commands.build_command(2, self.module_name, 'kickban', 'Kick and ban a user from a channel') + self.ctx.Commands.build_command(2, self.module_name, 'umode', 'Set user mode') + self.ctx.Commands.build_command(2, self.module_name, 'mode', 'Set channel mode') + self.ctx.Commands.build_command(2, self.module_name, 'get_mode', 'Retrieve current channel mode') + self.ctx.Commands.build_command(2, self.module_name, 'svsjoin', 'Force a user to join a channel') + self.ctx.Commands.build_command(2, self.module_name, 'svspart', 'Force a user to leave a channel') + self.ctx.Commands.build_command(2, self.module_name, 'svsnick', 'Force a user to change their nickname') + self.ctx.Commands.build_command(2, self.module_name, 'wallops', 'Send a message to all operators') + self.ctx.Commands.build_command(2, self.module_name, 'globops', 'Send a global operator message') + self.ctx.Commands.build_command(2, self.module_name, 'gnotice', 'Send a global notice') + self.ctx.Commands.build_command(2, self.module_name, 'whois', 'Get information about a user') + self.ctx.Commands.build_command(2, self.module_name, 'names', 'List users in a channel') + self.ctx.Commands.build_command(2, self.module_name, 'invite', 'Invite a user to a channel') + self.ctx.Commands.build_command(2, self.module_name, 'inviteme', 'Invite yourself to a channel') + self.ctx.Commands.build_command(2, self.module_name, 'sajoin', 'Force yourself into a channel') + self.ctx.Commands.build_command(2, self.module_name, 'sapart', 'Force yourself to leave a channel') + self.ctx.Commands.build_command(2, self.module_name, 'kill', 'Disconnect a user from the server') + self.ctx.Commands.build_command(2, self.module_name, 'gline', 'Ban a user from the entire server') + self.ctx.Commands.build_command(2, self.module_name, 'ungline', 'Remove a global server ban') + self.ctx.Commands.build_command(2, self.module_name, 'kline', 'Ban a user based on their hostname') + self.ctx.Commands.build_command(2, self.module_name, 'unkline', 'Remove a K-line ban') + self.ctx.Commands.build_command(2, self.module_name, 'shun', 'Prevent a user from sending messages') + self.ctx.Commands.build_command(2, self.module_name, 'unshun', 'Remove a shun from a user') + self.ctx.Commands.build_command(2, self.module_name, 'glinelist', 'List all global bans') + self.ctx.Commands.build_command(2, self.module_name, 'shunlist', 'List all shunned users') + self.ctx.Commands.build_command(2, self.module_name, 'klinelist', 'List all K-line bans') + self.ctx.Commands.build_command(3, self.module_name, 'map', 'Show the server network map') def unload(self) -> None: self.ctx.Commands.drop_command_by_module(self.module_name) @@ -214,7 +214,7 @@ class Command(IModule): user_uid = self.ctx.User.clean_uid(cmd[5]) userObj: MUser = self.ctx.User.get_user(user_uid) channel_name = cmd[4] if self.ctx.Channel.is_valid_channel(cmd[4]) else None - client_obj = self.ctx.Client.get_Client(user_uid) + client_obj = self.ctx.Client.get_client(user_uid) nickname = userObj.nickname if userObj is not None else None if client_obj is not None: diff --git a/mods/jsonrpc/mod_jsonrpc.py b/mods/jsonrpc/mod_jsonrpc.py index b7f68ed..06cfd48 100644 --- a/mods/jsonrpc/mod_jsonrpc.py +++ b/mods/jsonrpc/mod_jsonrpc.py @@ -91,9 +91,9 @@ class Jsonrpc(IModule): self.is_streaming = False # Create module commands (Mandatory) - self.ctx.Irc.build_command(1, self.module_name, 'jsonrpc', 'Activate the JSON RPC Live connection [ON|OFF]') - self.ctx.Irc.build_command(1, self.module_name, 'jruser', 'Get Information about a user using JSON RPC') - self.ctx.Irc.build_command(1, self.module_name, 'jrinstances', 'Get number of instances') + self.ctx.Commands.build_command(1, self.module_name, 'jsonrpc', 'Activate the JSON RPC Live connection [ON|OFF]') + self.ctx.Commands.build_command(1, self.module_name, 'jruser', 'Get Information about a user using JSON RPC') + self.ctx.Commands.build_command(1, self.module_name, 'jrinstances', 'Get number of instances') try: self.Rpc = ConnectionFactory(self.ctx.Config.DEBUG_LEVEL).get(self.ctx.Config.JSONRPC_METHOD) @@ -138,7 +138,6 @@ class Jsonrpc(IModule): channel=self.ctx.Config.SERVICE_CHANLOG ) self.ctx.Base.create_asynctask(thds.thread_unsubscribe(self)) - # await self.update_configuration('jsonrpc', 0) self.ctx.Commands.drop_command_by_module(self.module_name) self.ctx.Logs.debug(f"Unloading {self.module_name}") return None diff --git a/mods/test/mod_test.py b/mods/test/mod_test.py index f4dd35f..91ab290 100644 --- a/mods/test/mod_test.py +++ b/mods/test/mod_test.py @@ -53,11 +53,11 @@ class Test(IModule): """ # Create module commands (Mandatory) - self.ctx.Irc.build_command(0, self.module_name, 'test-command', 'Execute a test command') - self.ctx.Irc.build_command(0, self.module_name, 'asyncio', 'Create a new asynchron task!') - self.ctx.Irc.build_command(1, self.module_name, 'test_level_1', 'Execute a level 1 test command') - self.ctx.Irc.build_command(2, self.module_name, 'test_level_2', 'Execute a level 2 test command') - self.ctx.Irc.build_command(3, self.module_name, 'test_level_3', 'Execute a level 3 test command') + self.ctx.Commands.build_command(0, self.module_name, 'test-command', 'Execute a test command') + self.ctx.Commands.build_command(0, self.module_name, 'asyncio', 'Create a new asynchron task!') + self.ctx.Commands.build_command(1, self.module_name, 'test_level_1', 'Execute a level 1 test command') + self.ctx.Commands.build_command(2, self.module_name, 'test_level_2', 'Execute a level 2 test command') + self.ctx.Commands.build_command(3, self.module_name, 'test_level_3', 'Execute a level 3 test command') # Build the default configuration model (Mandatory) self._mod_config = self.ModConfModel(param_exemple1='str', param_exemple2=1) From 4c93f85008efdec8d11e9b352264de5f4132894f Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:19:29 +0100 Subject: [PATCH 4/7] Code refactoring on system modules. --- core/classes/modules/admin.py | 39 ++++++++++++--------------- core/classes/modules/client.py | 43 +++++++++--------------------- core/classes/modules/commands.py | 22 ++++++++++----- core/classes/modules/reputation.py | 14 +++++----- core/classes/modules/user.py | 18 +++++++------ core/definition.py | 19 +++++++++++++ 6 files changed, 81 insertions(+), 74 deletions(-) diff --git a/core/classes/modules/admin.py b/core/classes/modules/admin.py index a71a87d..c5f4304 100644 --- a/core/classes/modules/admin.py +++ b/core/classes/modules/admin.py @@ -14,12 +14,7 @@ class Admin: Args: loader (Loader): The Loader Instance. """ - self.Logs = loader.Logs - self.Base = loader.Base - self.Setting = loader.Settings - self.Config = loader.Config - self.User = loader.User - self.Definition = loader.Definition + self._ctx = loader def insert(self, new_admin: MAdmin) -> bool: """Insert a new admin object model @@ -33,11 +28,11 @@ class Admin: for record in self.UID_ADMIN_DB: if record.uid == new_admin.uid: - self.Logs.debug(f'{record.uid} already exist') + self._ctx.Logs.debug(f'{record.uid} already exist') return False self.UID_ADMIN_DB.append(new_admin) - self.Logs.debug(f'A new admin ({new_admin.nickname}) has been created') + self._ctx.Logs.debug(f'A new admin ({new_admin.nickname}) has been created') return True def update_nickname(self, uid: str, new_admin_nickname: str) -> bool: @@ -55,11 +50,11 @@ class Admin: if record.uid == uid: # If the admin exist, update and do not go further record.nickname = new_admin_nickname - self.Logs.debug(f'UID ({record.uid}) has been updated with new nickname {new_admin_nickname}') + self._ctx.Logs.debug(f'UID ({record.uid}) has been updated with new nickname {new_admin_nickname}') return True - self.Logs.debug(f'The new nickname {new_admin_nickname} was not updated, uid = {uid} - The Client is not an admin') + self._ctx.Logs.debug(f'The new nickname {new_admin_nickname} was not updated, uid = {uid} - The Client is not an admin') return False def update_level(self, nickname: str, new_admin_level: int) -> bool: @@ -77,10 +72,10 @@ class Admin: if record.nickname == nickname: # If the admin exist, update and do not go further record.level = new_admin_level - self.Logs.debug(f'Admin ({record.nickname}) has been updated with new level {new_admin_level}') + self._ctx.Logs.debug(f'Admin ({record.nickname}) has been updated with new level {new_admin_level}') return True - self.Logs.debug(f'The new level {new_admin_level} was not updated, nickname = {nickname} - The Client is not an admin') + self._ctx.Logs.debug(f'The new level {new_admin_level} was not updated, nickname = {nickname} - The Client is not an admin') return False @@ -96,10 +91,10 @@ class Admin: admin_obj = self.get_admin(uidornickname) if admin_obj: self.UID_ADMIN_DB.remove(admin_obj) - self.Logs.debug(f'UID ({admin_obj.uid}) has been deleted') + self._ctx.Logs.debug(f'UID ({admin_obj.uid}) has been deleted') return True - self.Logs.debug(f'The UID {uidornickname} was not deleted') + self._ctx.Logs.debug(f'The UID {uidornickname} was not deleted') return False @@ -186,20 +181,20 @@ class Admin: if fp is None: return False - query = f"SELECT user, level, language FROM {self.Config.TABLE_ADMIN} WHERE fingerprint = :fp" + query = f"SELECT user, level, language FROM {self._ctx.Config.TABLE_ADMIN} WHERE fingerprint = :fp" data = {'fp': fp} - exe = await self.Base.db_execute_query(query, data) + exe = await self._ctx.Base.db_execute_query(query, data) result = exe.fetchone() if result: account = result[0] level = result[1] language = result[2] - user_obj = self.User.get_user(uidornickname) + user_obj = self._ctx.User.get_user(uidornickname) if user_obj: - admin_obj = self.Definition.MAdmin(**user_obj.to_dict(), account=account, level=level, language=language) + admin_obj = self._ctx.Definition.MAdmin(**user_obj.to_dict(), account=account, level=level, language=language) if self.insert(admin_obj): - self.Setting.current_admin = admin_obj - self.Logs.debug(f"[Fingerprint login] {user_obj.nickname} ({admin_obj.account}) has been logged in successfully!") + self._ctx.Settings.current_admin = admin_obj + self._ctx.Logs.debug(f"[Fingerprint login] {user_obj.nickname} ({admin_obj.account}) has been logged in successfully!") return True return False @@ -215,8 +210,8 @@ class Admin: """ mes_donnees = {'admin': admin_nickname} - query_search_user = f"SELECT id FROM {self.Config.TABLE_ADMIN} WHERE user = :admin" - r = await self.Base.db_execute_query(query_search_user, mes_donnees) + query_search_user = f"SELECT id FROM {self._ctx.Config.TABLE_ADMIN} WHERE user = :admin" + r = await self._ctx.Base.db_execute_query(query_search_user, mes_donnees) exist_user = r.fetchone() if exist_user: return True diff --git a/core/classes/modules/client.py b/core/classes/modules/client.py index ec422eb..75917a8 100644 --- a/core/classes/modules/client.py +++ b/core/classes/modules/client.py @@ -15,8 +15,7 @@ class Client: Args: loader (Loader): The Loader instance. """ - self.Logs = loader.Logs - self.Base = loader.Base + self._ctx = loader def insert(self, new_client: 'MClient') -> bool: """Insert a new User object @@ -28,7 +27,7 @@ class Client: bool: True if inserted """ - client_obj = self.get_Client(new_client.uid) + client_obj = self.get_client(new_client.uid) if not client_obj is None: # User already created return False @@ -48,7 +47,7 @@ class Client: Returns: bool: True if updated """ - user_obj = self.get_Client(uidornickname=uid) + user_obj = self.get_client(uidornickname=uid) if user_obj is None: return False @@ -68,7 +67,7 @@ class Client: bool: True if user mode has been updaed """ response = True - user_obj = self.get_Client(uidornickname=uidornickname) + user_obj = self.get_client(uidornickname=uidornickname) if user_obj is None: return False @@ -93,7 +92,7 @@ class Client: return False liste_umodes = list(umodes) - final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] + final_umodes_liste = [x for x in self._ctx.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes = ''.join(final_umodes_liste) user_obj.umodes = f"+{final_umodes}" @@ -110,7 +109,7 @@ class Client: bool: True if deleted """ - user_obj = self.get_Client(uidornickname=uid) + user_obj = self.get_client(uidornickname=uid) if user_obj is None: return False @@ -119,7 +118,7 @@ class Client: return True - def get_Client(self, uidornickname: str) -> Optional['MClient']: + def get_client(self, uidornickname: str) -> Optional['MClient']: """Get The Client Object model Args: @@ -146,7 +145,7 @@ class Client: str|None: Return the UID """ - client_obj = self.get_Client(uidornickname=uidornickname) + client_obj = self.get_client(uidornickname=uidornickname) if client_obj is None: return None @@ -162,29 +161,13 @@ class Client: Returns: str|None: the nickname """ - client_obj = self.get_Client(uidornickname=uidornickname) + client_obj = self.get_client(uidornickname=uidornickname) if client_obj is None: return None return client_obj.nickname - def get_client_asdict(self, uidornickname: str) -> Optional[dict[str, Any]]: - """Transform User Object to a dictionary - - Args: - uidornickname (str): The UID or The nickname - - Returns: - Union[dict[str, any], None]: User Object as a dictionary or None - """ - client_obj = self.get_Client(uidornickname=uidornickname) - - if client_obj is None: - return None - - return client_obj.to_dict() - def is_exist(self, uidornickname: str) -> bool: """Check if the UID or the nickname exist in the USER DB @@ -194,7 +177,7 @@ class Client: Returns: bool: True if exist """ - user_obj = self.get_Client(uidornickname=uidornickname) + user_obj = self.get_client(uidornickname=uidornickname) if user_obj is None: return False @@ -211,15 +194,15 @@ class Client: bool: True if exist """ - table_client = self.Base.Config.TABLE_CLIENT + table_client = self._ctx.Base.Config.TABLE_CLIENT account_to_check = {'account': account.lower()} - account_to_check_query = await self.Base.db_execute_query(f""" + account_to_check_query = await self._ctx.Base.db_execute_query(f""" SELECT id FROM {table_client} WHERE LOWER(account) = :account """, account_to_check) account_to_check_result = account_to_check_query.fetchone() if account_to_check_result: - self.Logs.error(f"Account ({account}) already exist") + self._ctx.Logs.error(f"Account ({account}) already exist") return True return False diff --git a/core/classes/modules/commands.py b/core/classes/modules/commands.py index 412756a..2129061 100644 --- a/core/classes/modules/commands.py +++ b/core/classes/modules/commands.py @@ -13,11 +13,21 @@ class Command: Args: loader (Loader): The Loader instance. """ - self.Loader = loader - self.Base = loader.Base - self.Logs = loader.Logs + self._ctx = loader - def build(self, new_command_obj: MCommand) -> bool: + def build_command(self, level: int, module_name: str, command_name: str, command_description: str) -> bool: + """This method build the commands variable + + Args: + level (int): The Level of the command + module_name (str): The module name + command_name (str): The command name + command_description (str): The description of the command + """ + # Build Model. + return self._build(self._ctx.Definition.MCommand(module_name, command_name, command_description, level)) + + def _build(self, new_command_obj: MCommand) -> bool: command = self.get_command(new_command_obj.command_name, new_command_obj.module_name) if command is None: @@ -68,7 +78,7 @@ class Command: for c in tmp_model: self.DB_COMMANDS.remove(c) - self.Logs.debug(f"[COMMAND] Drop command for module {module_name}") + self._ctx.Logs.debug(f"[COMMAND] Drop command for module {module_name}") return True def get_ordered_commands(self) -> list[MCommand]: @@ -86,7 +96,7 @@ class Command: return new_list def is_client_allowed_to_run_command(self, nickname: str, command_name: str) -> bool: - admin = self.Loader.Admin.get_admin(nickname) + admin = self._ctx.Admin.get_admin(nickname) admin_level = admin.level if admin else 0 commands = self.get_commands_by_level(admin_level) diff --git a/core/classes/modules/reputation.py b/core/classes/modules/reputation.py index 242290a..84727ee 100644 --- a/core/classes/modules/reputation.py +++ b/core/classes/modules/reputation.py @@ -14,9 +14,7 @@ class Reputation: Args: loader (Loader): The Loader instance. """ - - self.Logs = loader.Logs - self.MReputation: Optional[MReputation] = None + self._ctx = loader def insert(self, new_reputation_user: MReputation) -> bool: """Insert a new Reputation User object @@ -34,16 +32,16 @@ class Reputation: if record.uid == new_reputation_user.uid: # If the user exist then return False and do not go further exist = True - self.Logs.debug(f'{record.uid} already exist') + self._ctx.Logs.debug(f'{record.uid} already exist') return result if not exist: self.UID_REPUTATION_DB.append(new_reputation_user) result = True - self.Logs.debug(f'New Reputation User Captured: ({new_reputation_user})') + self._ctx.Logs.debug(f'New Reputation User Captured: ({new_reputation_user})') if not result: - self.Logs.critical(f'The Reputation User Object was not inserted {new_reputation_user}') + self._ctx.Logs.critical(f'The Reputation User Object was not inserted {new_reputation_user}') return result @@ -86,11 +84,11 @@ class Reputation: # If the user exist then remove and return True and do not go further self.UID_REPUTATION_DB.remove(record) result = True - self.Logs.debug(f'UID ({record.uid}) has been deleted') + self._ctx.Logs.debug(f'UID ({record.uid}) has been deleted') return result if not result: - self.Logs.critical(f'The UID {uid} was not deleted') + self._ctx.Logs.critical(f'The UID {uid} was not deleted') return result diff --git a/core/classes/modules/user.py b/core/classes/modules/user.py index aff0963..154ec75 100644 --- a/core/classes/modules/user.py +++ b/core/classes/modules/user.py @@ -11,14 +11,16 @@ class User: UID_DB: list['MUser'] = [] @property - def get_current_user(self) -> 'MUser': - return self.current_user + def current_user(self) -> 'MUser': + return self._current_user + + @current_user.setter + def current_user(self, muser: 'MUser') -> None: + self._current_user = muser def __init__(self, loader: 'Loader'): - - self.Logs = loader.Logs - self.Base = loader.Base - self.current_user: Optional['MUser'] = None + self._ctx = loader + self._current_user: Optional['MUser'] = None def insert(self, new_user: 'MUser') -> bool: """Insert a new User object @@ -55,7 +57,7 @@ class User: return False user_obj.nickname = new_nickname - self.Logs.debug(f"UID ({uid}) has benn update with new nickname ({new_nickname}).") + self._ctx.Logs.debug(f"UID ({uid}) has benn update with new nickname ({new_nickname}).") return True def update_mode(self, uidornickname: str, modes: str) -> bool: @@ -94,7 +96,7 @@ class User: return False liste_umodes = list(umodes) - final_umodes_liste = [x for x in self.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] + final_umodes_liste = [x for x in self._ctx.Base.Settings.PROTOCTL_USER_MODES if x in liste_umodes] final_umodes = ''.join(final_umodes_liste) user_obj.umodes = f"+{final_umodes}" diff --git a/core/definition.py b/core/definition.py index 6d9ef95..3679c8d 100644 --- a/core/definition.py +++ b/core/definition.py @@ -1,3 +1,7 @@ +import asyncio +import concurrent +import concurrent.futures +import threading from datetime import datetime from json import dumps from dataclasses import dataclass, field, asdict, fields, replace @@ -210,6 +214,12 @@ class MConfig(MainModel): PASSWORD: str = "password" """The password of the admin of the service""" + RPC_HOST: str = "127.0.0.1" + """The host to bind. Default: 127.0.0.1""" + + RPC_PORT: int = 5000 + """The port of the defender json rpc. Default: 5000""" + RPC_USERS: list[dict] = field(default_factory=list) """The Defender rpc users""" @@ -344,6 +354,15 @@ class MConfig(MainModel): self.SERVEUR_CHARSET: list = ["utf-8", "iso-8859-1"] """0: utf-8 | 1: iso-8859-1""" +@dataclass +class MThread(MainModel): + name: str + thread_id: Optional[int] + thread_event: Optional[threading.Event] + thread_obj: threading.Thread + executor: concurrent.futures.ThreadPoolExecutor + future: asyncio.Future + @dataclass class MCommand(MainModel): module_name: str = None From eb7c6ef8d0cf7c6104d36206efcc50c0230d8e88 Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:20:56 +0100 Subject: [PATCH 5/7] Code refactoring --- core/classes/modules/translation.py | 2 +- core/utils.py | 56 ++++++++++------------------- defender.py | 3 +- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/core/classes/modules/translation.py b/core/classes/modules/translation.py index 6e0bad7..bf15d83 100644 --- a/core/classes/modules/translation.py +++ b/core/classes/modules/translation.py @@ -93,4 +93,4 @@ class Translation: except Exception as err: self.Logs.error(f'General Error: {err}') - return {} + return dict() diff --git a/core/utils.py b/core/utils.py index a887214..28af29f 100644 --- a/core/utils.py +++ b/core/utils.py @@ -3,20 +3,20 @@ Main utils library. """ import gc import ssl -import socket -import sys from pathlib import Path from re import match, sub +import threading from typing import Literal, Optional, Any, TYPE_CHECKING from datetime import datetime -from time import time +from time import time, sleep from random import choice from hashlib import md5, sha3_512 from core.classes.modules.settings import global_settings from asyncio import iscoroutinefunction if TYPE_CHECKING: - from core.irc import Irc + from threading import Event + from core.loader import Loader def tr(message: str, *args) -> str: """Translation Engine system @@ -115,39 +115,6 @@ def get_ssl_context() -> ssl.SSLContext: ctx.verify_mode = ssl.CERT_NONE return ctx -def create_socket(uplink: 'Irc') -> None: - """Create a socket to connect SSL or Normal connection - """ - try: - soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK) - connexion_information = (uplink.Config.SERVEUR_IP, uplink.Config.SERVEUR_PORT) - - if uplink.Config.SERVEUR_SSL: - # Create SSL Context object - ssl_context = get_ssl_context() - ssl_connexion = ssl_context.wrap_socket(soc, server_hostname=uplink.Config.SERVEUR_HOSTNAME) - ssl_connexion.connect(connexion_information) - uplink.IrcSocket = ssl_connexion - uplink.Config.SSL_VERSION = uplink.IrcSocket.version() - uplink.Logs.info(f"-- Connected using SSL : Version = {uplink.Config.SSL_VERSION}") - else: - soc.connect(connexion_information) - uplink.IrcSocket = soc - uplink.Logs.info("-- Connected in a normal mode!") - - return None - - except (ssl.SSLEOFError, ssl.SSLError) as soe: - uplink.Logs.critical(f"[SSL ERROR]: {soe}") - except OSError as oe: - uplink.Logs.critical(f"[OS Error]: {oe}") - if 'connection refused' in str(oe).lower(): - sys.exit(oe.__str__()) - if oe.errno == 10053: - sys.exit(oe.__str__()) - except AttributeError as ae: - uplink.Logs.critical(f"AttributeError: {ae}") - def run_python_garbage_collector() -> int: """Run Python garbage collector @@ -167,6 +134,21 @@ def get_number_gc_objects(your_object_to_count: Optional[Any] = None) -> int: return sum(1 for obj in gc.get_objects() if isinstance(obj, your_object_to_count)) +def heartbeat(event: 'Event', loader: 'Loader', beat: float) -> None: + """Execute certaines commandes de nettoyage toutes les x secondes + x étant définit a l'initialisation de cette class (self.beat) + + Args: + beat (float): Nombre de secondes entre chaque exécution + """ + + while event.is_set(): + loader.Base.execute_periodic_action() + sleep(beat) + + loader.Logs.debug("Heartbeat is off!") + return None + def generate_random_string(lenght: int) -> str: """Retourn une chaîne aléatoire en fonction de la longueur spécifiée. diff --git a/defender.py b/defender.py index e6aeb98..ee0bd3c 100644 --- a/defender.py +++ b/defender.py @@ -1,6 +1,5 @@ import asyncio from core import install - ############################################# # @Version : 6.4 # # Requierements : # @@ -19,4 +18,4 @@ async def main(): await loader.Irc.run() if __name__ == "__main__": - asyncio.run(main()) + asyncio.run(main(), debug=False) From 5938a1511b60713b6abeacfd1d1cbdb10f02722e Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:22:11 +0100 Subject: [PATCH 6/7] Fix asyncio unwaitable methods --- core/classes/modules/rpc/__init__.py | 3 ++- core/classes/modules/settings.py | 8 +++++--- core/classes/protocols/unreal6.py | 7 ++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/core/classes/modules/rpc/__init__.py b/core/classes/modules/rpc/__init__.py index 75977e6..fcfae54 100644 --- a/core/classes/modules/rpc/__init__.py +++ b/core/classes/modules/rpc/__init__.py @@ -1 +1,2 @@ -__version__ = '1.0.0' \ No newline at end of file +__version__ = '1.0.0' +__all__ = ['start_rpc_server', 'stop_rpc_server'] \ No newline at end of file diff --git a/core/classes/modules/settings.py b/core/classes/modules/settings.py index 216920e..fd6330a 100644 --- a/core/classes/modules/settings.py +++ b/core/classes/modules/settings.py @@ -6,7 +6,7 @@ from threading import Timer, Thread, RLock from asyncio.locks import Lock from socket import socket from typing import Any, Optional, TYPE_CHECKING -from core.definition import MSModule, MAdmin +from core.definition import MSModule, MAdmin, MThread if TYPE_CHECKING: from core.classes.modules.user import User @@ -19,10 +19,12 @@ class Settings: RUNNING_TIMERS: list[Timer] = [] RUNNING_THREADS: list[Thread] = [] - RUNNING_ASYNCTASKS: list[asyncio.Task] = [] RUNNING_SOCKETS: list[socket] = [] + RUNNING_ASYNC_TASKS: list[asyncio.Task] = [] + RUNNING_ASYNC_THREADS: list[MThread] = [] PERIODIC_FUNC: dict[str, Any] = {} - LOCK: RLock = RLock() + + THLOCK: RLock = RLock() AILOCK: Lock = Lock() CONSOLE: bool = False diff --git a/core/classes/protocols/unreal6.py b/core/classes/protocols/unreal6.py index b6dc9ef..1c3634c 100644 --- a/core/classes/protocols/unreal6.py +++ b/core/classes/protocols/unreal6.py @@ -4,7 +4,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Any, Optional from ssl import SSLEOFError, SSLError from core.classes.interfaces.iprotocol import IProtocol -from core.utils import tr +from core.utils import is_coroutinefunction, tr if TYPE_CHECKING: from core.definition import MClient, MSasl, MUser, MChannel @@ -258,6 +258,7 @@ class Unrealircd6(IProtocol): async def send_set_mode(self, modes: str, *, nickname: Optional[str] = None, channel_name: Optional[str] = None, params: Optional[str] = None) -> None: """Set a mode to channel or to a nickname or for a user in a channel + This method will always send as the command as Defender's nickname (service_id) Args: modes (str): The selected mode @@ -478,7 +479,7 @@ class Unrealircd6(IProtocol): c_uid = client_obj.uid c_nickname = client_obj.nickname await self.send2socket(f":{self._ctx.Config.SERVEUR_LINK} SVSLOGIN {self._ctx.Settings.MAIN_SERVER_HOSTNAME} {c_uid} 0") - self.send_svs2mode(c_nickname, '-r') + await self.send_svs2mode(c_nickname, '-r') except Exception as err: self._ctx.Logs.error(f'General Error: {err}') @@ -1020,7 +1021,7 @@ class Unrealircd6(IProtocol): # Send EOF to other modules for module in self._ctx.ModuleUtils.model_get_loaded_modules().copy(): - module.class_instance.cmd(server_msg_copy) + await module.class_instance.cmd(server_msg_copy) if self._ctx.Utils.is_coroutinefunction(module.class_instance.cmd) else module.class_instance.cmd(server_msg_copy) # Join saved channels & load existing modules await self._ctx.Channel.db_join_saved_channels() From cbe527d7d99161b20c8c35602862ed879cd4118b Mon Sep 17 00:00:00 2001 From: adator <85586985+adator85@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:22:54 +0100 Subject: [PATCH 7/7] Code Refactoring --- core/base.py | 174 +++++++++---------- core/classes/modules/rehash.py | 66 ++++++- core/classes/modules/rpc/rpc.py | 12 +- core/irc.py | 13 +- core/loader.py | 2 +- mods/defender/mod_defender.py | 121 ++++++++----- mods/defender/schemas.py | 14 +- mods/defender/threads.py | 255 ++++++++++++++++++++++----- mods/defender/utils.py | 298 ++++++++++---------------------- mods/jsonrpc/threads.py | 1 - 10 files changed, 558 insertions(+), 398 deletions(-) diff --git a/core/base.py b/core/base.py index 801c051..a16faca 100644 --- a/core/base.py +++ b/core/base.py @@ -2,17 +2,18 @@ import asyncio import os import re import json -import time import socket import threading import ipaddress import ast import requests +import concurrent.futures from dataclasses import fields -from typing import Any, Callable, Iterable, Optional, TYPE_CHECKING +from typing import Any, Awaitable, Callable, Optional, TYPE_CHECKING, Union from base64 import b64decode, b64encode from sqlalchemy import create_engine, Engine, Connection, CursorResult from sqlalchemy.sql import text +import core.definition as dfn if TYPE_CHECKING: from core.loader import Loader @@ -36,12 +37,15 @@ class Base: # Liste des threads en cours self.running_threads: list[threading.Thread] = self.Settings.RUNNING_THREADS - # List of all async tasks - self.running_asynctasks: list[asyncio.Task] = self.Settings.RUNNING_ASYNCTASKS - # Les sockets ouvert self.running_sockets: list[socket.socket] = self.Settings.RUNNING_SOCKETS + # List of all asyncio tasks + self.running_iotasks: list[asyncio.Task] = self.Settings.RUNNING_ASYNC_TASKS + + # List of all asyncio threads pool executors + self.running_iothreads: list[dfn.MThread] = self.Settings.RUNNING_ASYNC_THREADS + # Liste des fonctions en attentes self.periodic_func: dict[object] = self.Settings.PERIODIC_FUNC @@ -71,32 +75,32 @@ class Base: return None def __get_latest_defender_version(self) -> None: - try: - self.logs.debug(f'-- Looking for a new version available on Github') - token = '' - json_url = f'https://raw.githubusercontent.com/adator85/DEFENDER/main/version.json' - headers = { - 'Authorization': f'token {token}', - 'Accept': 'application/vnd.github.v3.raw' # Indique à GitHub que nous voulons le contenu brut du fichier - } + self.logs.debug(f'-- Looking for a new version available on Github') + token = '' + json_url = f'https://raw.githubusercontent.com/adator85/DEFENDER/main/version.json' + headers = { + 'Authorization': f'token {token}', + 'Accept': 'application/vnd.github.v3.raw' # Indique à GitHub que nous voulons le contenu brut du fichier + } - if token == '': - response = requests.get(json_url, timeout=self.Config.API_TIMEOUT) - else: - response = requests.get(json_url, headers=headers, timeout=self.Config.API_TIMEOUT) + with requests.Session() as sess: + try: + if token == '': + response = sess.get(json_url, timeout=self.Config.API_TIMEOUT) + else: + response = sess.get(json_url, headers=headers, timeout=self.Config.API_TIMEOUT) - response.raise_for_status() # Vérifie si la requête a réussi - json_response:dict = response.json() - # self.LATEST_DEFENDER_VERSION = json_response["version"] - self.Config.LATEST_VERSION = json_response['version'] + response.raise_for_status() # Vérifie si la requête a réussi + json_response:dict = response.json() + self.Config.LATEST_VERSION = json_response.get('version', '') + return None - return None - except requests.HTTPError as err: - self.logs.error(f'Github not available to fetch latest version: {err}') - except: - self.logs.warning(f'Github not available to fetch latest version') + except requests.HTTPError as err: + self.logs.error(f'Github not available to fetch latest version: {err}') + except: + self.logs.warning(f'Github not available to fetch latest version') - def check_for_new_version(self, online:bool) -> bool: + def check_for_new_version(self, online: bool) -> bool: """Check if there is a new version available Args: @@ -359,8 +363,8 @@ class Base: except Exception as err: self.logs.error(err, exc_info=True) - def create_asynctask(self, func: Any, *, async_name: str = None, run_once: bool = False) -> asyncio.Task: - """Create a new asynchrone and store it into running_asynctasks variable + def create_asynctask(self, func: Callable[..., Awaitable[Any]], *, async_name: str = None, run_once: bool = False) -> Optional[asyncio.Task]: + """Create a new asynchrone and store it into running_iotasks variable Args: func (Callable): The function you want to call in asynchrone way @@ -379,26 +383,71 @@ class Base: task = asyncio.create_task(func, name=name) task.add_done_callback(self.asynctask_done) - self.running_asynctasks.append(task) + self.running_iotasks.append(task) - self.logs.debug(f"++ New asynchrone task created as: {task.get_name()}") + self.logs.debug(f"=== New IO task created as: {task.get_name()}") return task - def asynctask_done(self, task: asyncio.Task): + async def create_thread_io(self, func: Callable[..., Any], *args, run_once: bool = False, thread_flag: bool = False) -> Optional[Any]: + """Run threads via asyncio. + + Args: + func (Callable[..., Any]): The blocking IO function + run_once (bool, optional): If it should be run once.. Defaults to False. + thread_flag (bool, optional): If you are using a endless loop, use the threading Event object. Defaults to False. + + Returns: + Any: The final result of the blocking IO function + """ + if run_once: + for iothread in self.running_iothreads: + if func.__name__.lower() == iothread.name.lower(): + return None + + with concurrent.futures.ThreadPoolExecutor(max_workers=1, thread_name_prefix=func.__name__) as executor: + loop = asyncio.get_event_loop() + largs = list(args) + thread_event: Optional[threading.Event] = None + if thread_flag: + thread_event = threading.Event() + thread_event.set() + largs.insert(0, thread_event) + + future = loop.run_in_executor(executor, func, *tuple(largs)) + future.add_done_callback(self.asynctask_done) + + id_obj = self.Loader.Definition.MThread( + name=func.__name__, + thread_id=list(executor._threads)[0].native_id, + thread_event=thread_event, + thread_obj=list(executor._threads)[0], + executor=executor, + future=future) + + self.running_iothreads.append(id_obj) + self.logs.debug(f"=== New thread started {func.__name__} with max workers set to: {executor._max_workers}") + result = await future + + self.running_iothreads.remove(id_obj) + return result + + def asynctask_done(self, task: Union[asyncio.Task, asyncio.Future]): """Log task when done Args: task (asyncio.Task): The Asyncio Task callback """ + name = task.get_name() if isinstance(task, asyncio.Task) else "Thread" + task_or_future = "Task" if isinstance(task, asyncio.Task) else "Future" try: if task.exception(): - self.logs.error(f"[ASYNCIO] Task {task.get_name()} failed with exception: {task.exception()}") + self.logs.error(f"[ASYNCIO] {task_or_future} {name} failed with exception: {task.exception()}") else: - self.logs.debug(f"[ASYNCIO] Task {task.get_name()} completed successfully.") + self.logs.debug(f"[ASYNCIO] {task_or_future} {name} completed successfully.") except asyncio.CancelledError as ce: - self.logs.debug(f"[ASYNCIO] Task {task.get_name()} terminated with cancelled error.") + self.logs.debug(f"[ASYNCIO] {task_or_future} {name} terminated with cancelled error. {ce}") except asyncio.InvalidStateError as ie: - self.logs.debug(f"[ASYNCIO] Task {task.get_name()} terminated with invalid state error.") + self.logs.debug(f"[ASYNCIO] {task_or_future} {name} terminated with invalid state error. {ie}") def is_thread_alive(self, thread_name: str) -> bool: """Check if the thread is still running! using the is_alive method of Threads. @@ -492,59 +541,6 @@ class Base: self.running_sockets.remove(soc) self.logs.debug(f"-- Socket ==> closed {str(soc.fileno())}") - async def shutdown(self) -> None: - """Methode qui va préparer l'arrêt complêt du service - """ - # Stop RpcServer if running - await self.Loader.RpcServer.stop_server() - - # unload modules. - self.logs.debug(f"=======> Unloading all modules!") - for module in self.Loader.ModuleUtils.model_get_loaded_modules().copy(): - await self.Loader.ModuleUtils.unload_one_module(module.module_name) - - self.logs.debug(f"=======> Closing all Coroutines!") - try: - await asyncio.wait_for(asyncio.gather(*self.running_asynctasks), timeout=5) - except asyncio.exceptions.TimeoutError as te: - self.logs.debug(f"Asyncio Timeout reached! {te}") - for task in self.running_asynctasks: - task.cancel() - except asyncio.exceptions.CancelledError as cerr: - self.logs.debug(f"Asyncio CancelledError reached! {cerr}") - - - # Nettoyage des timers - self.logs.debug(f"=======> Checking for Timers to stop") - for timer in self.running_timers: - while timer.is_alive(): - self.logs.debug(f"> waiting for {timer.name} to close") - timer.cancel() - await asyncio.sleep(0.2) - self.running_timers.remove(timer) - self.logs.debug(f"> Cancelling {timer.name} {timer.native_id}") - - self.logs.debug(f"=======> Checking for Threads to stop") - for thread in self.running_threads: - if thread.name == 'heartbeat' and thread.is_alive(): - self.execute_periodic_action() - self.logs.debug(f"> Running the last periodic action") - self.running_threads.remove(thread) - self.logs.debug(f"> Cancelling {thread.name} {thread.native_id}") - - self.logs.debug(f"=======> Checking for Sockets to stop") - for soc in self.running_sockets: - soc.close() - while soc.fileno() != -1: - soc.close() - - self.running_sockets.remove(soc) - self.logs.debug(f"> Socket ==> closed {str(soc.fileno())}") - - self.db_close() - - return None - def db_init(self) -> tuple[Engine, Connection]: db_directory = self.Config.DB_PATH diff --git a/core/classes/modules/rehash.py b/core/classes/modules/rehash.py index 791fe9b..8c646e4 100644 --- a/core/classes/modules/rehash.py +++ b/core/classes/modules/rehash.py @@ -1,3 +1,4 @@ +import asyncio import importlib import sys import time @@ -26,7 +27,6 @@ REHASH_MODULES = [ 'core.classes.protocols.inspircd' ] - async def restart_service(uplink: 'Loader', reason: str = "Restarting with no reason!") -> None: """ @@ -69,7 +69,7 @@ async def rehash_service(uplink: 'Loader', nickname: str) -> None: need_a_restart = ["SERVEUR_ID"] uplink.Settings.set_cache('db_commands', uplink.Commands.DB_COMMANDS) - await uplink.RpcServer.stop_server() + await uplink.RpcServer.stop_rpc_server() restart_flag = False config_model_bakcup = uplink.Config @@ -122,10 +122,68 @@ async def rehash_service(uplink: 'Loader', nickname: str) -> None: uplink.Irc.Protocol.register_command() uplink.RpcServer = uplink.RpcServerModule.JSonRpcServer(uplink) - uplink.Base.create_asynctask(uplink.RpcServer.start_server()) + uplink.Base.create_asynctask(uplink.RpcServer.start_rpc_server()) # Reload Service modules for module in uplink.ModuleUtils.model_get_loaded_modules().copy(): await uplink.ModuleUtils.reload_one_module(module.module_name, nickname) - return None \ No newline at end of file + return None + +async def shutdown(uplink: 'Loader') -> None: + """Methode qui va préparer l'arrêt complêt du service + """ + # Stop RpcServer if running + await uplink.RpcServer.stop_rpc_server() + + # unload modules. + uplink.Logs.debug(f"=======> Unloading all modules!") + for module in uplink.ModuleUtils.model_get_loaded_modules().copy(): + await uplink.ModuleUtils.unload_one_module(module.module_name) + + # Nettoyage des timers + uplink.Logs.debug(f"=======> Closing all timers!") + for timer in uplink.Base.running_timers: + while timer.is_alive(): + uplink.Logs.debug(f"> waiting for {timer.name} to close") + timer.cancel() + await asyncio.sleep(0.2) + uplink.Logs.debug(f"> Cancelling {timer.name} {timer.native_id}") + + uplink.Logs.debug(f"=======> Closing all Threads!") + for thread in uplink.Base.running_threads: + if thread.name == 'heartbeat' and thread.is_alive(): + uplink.Base.execute_periodic_action() + uplink.Logs.debug(f"> Running the last periodic action") + uplink.Logs.debug(f"> Cancelling {thread.name} {thread.native_id}") + + uplink.Logs.debug(f"=======> Closing all IO Threads!") + [th.thread_event.clear() for th in uplink.Base.running_iothreads] + + uplink.Logs.debug(f"=======> Closing all IO TASKS!") + try: + await asyncio.wait_for(asyncio.gather(*uplink.Base.running_iotasks), timeout=5) + except asyncio.exceptions.TimeoutError as te: + uplink.Logs.debug(f"Asyncio Timeout reached! {te}") + for task in uplink.Base.running_iotasks: + task.cancel() + except asyncio.exceptions.CancelledError as cerr: + uplink.Logs.debug(f"Asyncio CancelledError reached! {cerr}") + + uplink.Logs.debug(f"=======> Closing all Sockets!") + for soc in uplink.Base.running_sockets: + soc.close() + while soc.fileno() != -1: + soc.close() + uplink.Base.running_sockets.remove(soc) + uplink.Logs.debug(f"> Socket ==> closed {str(soc.fileno())}") + + uplink.Base.running_timers.clear() + uplink.Base.running_threads.clear() + uplink.Base.running_iotasks.clear() + uplink.Base.running_iothreads.clear() + uplink.Base.running_sockets.clear() + + uplink.Base.db_close() + + return None \ No newline at end of file diff --git a/core/classes/modules/rpc/rpc.py b/core/classes/modules/rpc/rpc.py index 39b7050..ace50b0 100644 --- a/core/classes/modules/rpc/rpc.py +++ b/core/classes/modules/rpc/rpc.py @@ -17,11 +17,11 @@ if TYPE_CHECKING: class JSonRpcServer: - def __init__(self, context: 'Loader', *, hostname: str = '0.0.0.0', port: int = 5000): + def __init__(self, context: 'Loader'): self._ctx = context self.live: bool = False - self.host = hostname - self.port = port + self.host = context.Config.RPC_HOST + self.port = context.Config.RPC_PORT self.routes: list[Route] = [] self.server: Optional[uvicorn.Server] = None @@ -34,12 +34,12 @@ class JSonRpcServer: 'command.get.by.module': RPCCommand(context).command_get_by_module } - async def start_server(self): + async def start_rpc_server(self): if not self.live: self.routes = [Route('/api', self.request_handler, methods=['POST'])] self.app_jsonrpc = Starlette(debug=False, routes=self.routes) - config = uvicorn.Config(self.app_jsonrpc, host=self.host, port=self.port, log_level=self._ctx.Config.DEBUG_LEVEL) + config = uvicorn.Config(self.app_jsonrpc, host=self.host, port=self.port, log_level=self._ctx.Config.DEBUG_LEVEL+10) self.server = uvicorn.Server(config) self.live = True await self._ctx.Irc.Protocol.send_priv_msg( @@ -52,7 +52,7 @@ class JSonRpcServer: else: self._ctx.Logs.debug("Server already running") - async def stop_server(self): + async def stop_rpc_server(self): if self.server: self.server.should_exit = True diff --git a/core/irc.py b/core/irc.py index 8d4a980..e13f8af 100644 --- a/core/irc.py +++ b/core/irc.py @@ -126,7 +126,11 @@ class Irc: async def listen(self): self.ctx.Base.create_asynctask( - self.ctx.Base.create_thread_io(self.ctx.Utils.heartbeat, True, self.ctx, self.beat) + self.ctx.Base.create_thread_io( + self.ctx.Utils.heartbeat, + self.ctx, self.beat, + run_once=True, thread_flag=True + ) ) while self.signal: @@ -343,8 +347,13 @@ class Irc: async def thread_check_for_new_version(self, fromuser: str) -> None: dnickname = self.ctx.Config.SERVICE_NICKNAME + response = self.ctx.Base.create_asynctask( + self.ctx.Base.create_thread_io( + self.ctx.Base.check_for_new_version, True + ) + ) - if self.ctx.Base.check_for_new_version(True): + if response: await self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=f" New Version available : {self.ctx.Config.CURRENT_VERSION} >>> {self.ctx.Config.LATEST_VERSION}") await self.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, msg=" Please run (git pull origin main) in the current folder") else: diff --git a/core/loader.py b/core/loader.py index 50a9c6d..611fc9f 100644 --- a/core/loader.py +++ b/core/loader.py @@ -81,7 +81,7 @@ class Loader: self.PFactory: factory.ProtocolFactorty = factory.ProtocolFactorty(self) - self.RpcServer: rpc_mod.JSonRpcServer = rpc_mod.JSonRpcServer(self, hostname='0.0.0.0') + self.RpcServer: rpc_mod.JSonRpcServer = rpc_mod.JSonRpcServer(self) self.Logs.debug(self.Utils.tr("Loader %s success", __name__)) diff --git a/mods/defender/mod_defender.py b/mods/defender/mod_defender.py index 1127a3e..7d683d4 100644 --- a/mods/defender/mod_defender.py +++ b/mods/defender/mod_defender.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import logging from typing import Any, TYPE_CHECKING, Optional from core.classes.interfaces.imodule import IModule import mods.defender.schemas as schemas @@ -26,6 +27,8 @@ class Defender(IModule): def __init__(self, context: 'Loader') -> None: super().__init__(context) self._mod_config: Optional[schemas.ModConfModel] = None + self.Schemas = schemas.RepDB() + self.Threads = thds @property def mod_config(self) -> ModConfModel: @@ -55,40 +58,43 @@ class Defender(IModule): return None async def load(self): - # Variable qui va contenir les options de configuration du module Defender self._mod_config: schemas.ModConfModel = self.ModConfModel() # sync the database with local variable (Mandatory) await self.sync_db() - # Add module schemas - self.Schemas = schemas - # Add module utils functions self.mod_utils = utils - + # Create module commands (Mandatory) - self.ctx.Irc.build_command(0, self.module_name, 'code', 'Display the code or key for access') - self.ctx.Irc.build_command(1, self.module_name, 'info', 'Provide information about the channel or server') - self.ctx.Irc.build_command(1, self.module_name, 'autolimit', 'Automatically set channel user limits') - self.ctx.Irc.build_command(3, self.module_name, 'reputation', 'Check or manage user reputation') - self.ctx.Irc.build_command(3, self.module_name, 'proxy_scan', 'Scan users for proxy connections') - self.ctx.Irc.build_command(3, self.module_name, 'flood', 'Handle flood detection and mitigation') - self.ctx.Irc.build_command(3, self.module_name, 'status', 'Check the status of the server or bot') - self.ctx.Irc.build_command(3, self.module_name, 'show_reputation', 'Display reputation information') - self.ctx.Irc.build_command(3, self.module_name, 'sentinel', 'Monitor and guard the channel or server') + self.ctx.Commands.build_command(0, self.module_name, 'code', 'Display the code or key for access') + self.ctx.Commands.build_command(1, self.module_name, 'info', 'Provide information about the channel or server') + self.ctx.Commands.build_command(1, self.module_name, 'autolimit', 'Automatically set channel user limits') + self.ctx.Commands.build_command(3, self.module_name, 'reputation', 'Check or manage user reputation') + self.ctx.Commands.build_command(3, self.module_name, 'proxy_scan', 'Scan users for proxy connections') + self.ctx.Commands.build_command(3, self.module_name, 'flood', 'Handle flood detection and mitigation') + self.ctx.Commands.build_command(3, self.module_name, 'status', 'Check the status of the server or bot') + self.ctx.Commands.build_command(3, self.module_name, 'show_reputation', 'Display reputation information') + self.ctx.Commands.build_command(3, self.module_name, 'sentinel', 'Monitor and guard the channel or server') self.timeout = self.ctx.Config.API_TIMEOUT # Listes qui vont contenir les ip a scanner avec les différentes API - self.Schemas.DB_ABUSEIPDB_USERS = self.Schemas.DB_FREEIPAPI_USERS = self.Schemas.DB_CLOUDFILT_USERS = [] - self.Schemas.DB_PSUTIL_USERS = self.Schemas.DB_LOCALSCAN_USERS = [] + self.Schemas.DB_ABUSEIPDB_USERS = [] + self.Schemas.DB_FREEIPAPI_USERS = [] + self.Schemas.DB_CLOUDFILT_USERS = [] + self.Schemas.DB_PSUTIL_USERS = [] + self.Schemas.DB_LOCALSCAN_USERS = [] # Variables qui indique que les threads sont en cours d'éxecutions - self.abuseipdb_isRunning = self.freeipapi_isRunning = self.cloudfilt_isRunning = True - self.psutil_isRunning = self.localscan_isRunning = self.reputationTimer_isRunning = True - self.autolimit_isRunning = True + self.abuseipdb_isRunning = True if self.mod_config.abuseipdb_scan == 1 else False + self.freeipapi_isRunning = True if self.mod_config.freeipapi_scan == 1 else False + self.cloudfilt_isRunning = True if self.mod_config.cloudfilt_scan == 1 else False + self.psutil_isRunning = True if self.mod_config.psutil_scan == 1 else False + self.localscan_isRunning = True if self.mod_config.local_scan == 1 else False + self.reputationTimer_isRunning = True if self.mod_config.reputation == 1 else False + self.autolimit_isRunning = True if self.mod_config.autolimit == 1 else False # Variable qui va contenir les users self.flood_system = {} @@ -101,19 +107,21 @@ class Defender(IModule): self.cloudfilt_key = 'r1gEtjtfgRQjtNBDMxsg' # Démarrer les threads pour démarrer les api - self.ctx.Base.create_asynctask(thds.coro_freeipapi_scan(self)) - self.ctx.Base.create_asynctask(thds.coro_cloudfilt_scan(self)) - self.ctx.Base.create_asynctask(thds.coro_abuseipdb_scan(self)) - self.ctx.Base.create_asynctask(thds.coro_local_scan(self)) - self.ctx.Base.create_asynctask(thds.coro_psutil_scan(self)) - self.ctx.Base.create_asynctask(thds.coro_apply_reputation_sanctions(self)) - - if self.mod_config.autolimit == 1: - self.ctx.Base.create_asynctask(thds.coro_autolimit(self)) + self.ctx.Base.create_asynctask(self.Threads.coro_freeipapi_scan(self)) if self.mod_config.freeipapi_scan == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_cloudfilt_scan(self)) if self.mod_config.cloudfilt_scan == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_abuseipdb_scan(self)) if self.mod_config.abuseipdb_scan == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_local_scan(self)) if self.mod_config.local_scan == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_psutil_scan(self)) if self.mod_config.psutil_scan == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_apply_reputation_sanctions(self)) if self.mod_config.reputation == 1 else None + self.ctx.Base.create_asynctask(self.Threads.coro_autolimit(self)) if self.mod_config.autolimit == 1 else None if self.mod_config.reputation == 1: await self.ctx.Irc.Protocol.send_sjoin(self.ctx.Config.SALON_JAIL) await self.ctx.Irc.Protocol.send2socket(f":{self.ctx.Config.SERVICE_NICKNAME} SAMODE {self.ctx.Config.SALON_JAIL} +o {self.ctx.Config.SERVICE_NICKNAME}") + for chan in self.ctx.Channel.UID_CHANNEL_DB: + if chan.name != self.ctx.Config.SALON_JAIL: + await self.ctx.Irc.Protocol.send_set_mode('+b', channel_name=chan.name, params='~security-group:unknown-users') + await self.ctx.Irc.Protocol.send_set_mode('+eee', channel_name=chan.name, params='~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users') def __onload(self): @@ -138,7 +146,7 @@ class Defender(IModule): if localscan: self.Schemas.DB_LOCALSCAN_USERS = localscan - def unload(self) -> None: + async def unload(self) -> None: """Cette methode sera executée a chaque désactivation ou rechargement de module """ @@ -158,6 +166,13 @@ class Defender(IModule): self.ctx.Commands.drop_command_by_module(self.module_name) + if self.mod_config.reputation == 1: + await self.ctx.Irc.Protocol.send_part_chan(self.ctx.Config.SERVICE_ID, self.ctx.Config.SALON_JAIL) + for chan in self.ctx.Channel.UID_CHANNEL_DB: + if chan.name != self.ctx.Config.SALON_JAIL: + await self.ctx.Irc.Protocol.send_set_mode('-b', channel_name=chan.name, params='~security-group:unknown-users') + await self.ctx.Irc.Protocol.send_set_mode('-eee', channel_name=chan.name, params='~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users') + return None async def insert_db_trusted(self, uid: str, nickname:str) -> None: @@ -411,22 +426,26 @@ class Defender(IModule): if self.mod_config.reputation == 1: await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.ctx.Config.COLORS.green}REPUTATION{self.ctx.Config.COLORS.black} ] : Already activated", channel=dchanlog) - return False + return None - # self.update_db_configuration('reputation', 1) await self.update_configuration(key, 1) + self.ctx.Base.create_asynctask(self.Threads.coro_apply_reputation_sanctions(self)) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {self.ctx.Config.COLORS.green}REPUTATION{self.ctx.Config.COLORS.black} ] : Activated by {fromuser}", channel=dchanlog) await self.ctx.Irc.Protocol.send_join_chan(uidornickname=dnickname, channel=jail_chan) await self.ctx.Irc.Protocol.send2socket(f":{service_id} SAMODE {jail_chan} +{dumodes} {dnickname}") - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {jail_chan} +{jail_chan_mode}") + await self.ctx.Irc.Protocol.send_set_mode(f'+{jail_chan_mode}', channel_name=jail_chan) if self.mod_config.reputation_sg == 1: for chan in self.ctx.Channel.UID_CHANNEL_DB: if chan.name != jail_chan: - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {chan.name} +b ~security-group:unknown-users") - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {chan.name} +eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") + await self.ctx.Irc.Protocol.send_set_mode('+b', channel_name=chan.name, params='~security-group:unknown-users') + await self.ctx.Irc.Protocol.send_set_mode( + '+eee', + channel_name=chan.name, + params='~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users' + ) await self.ctx.Channel.db_query_channel('add', self.module_name, jail_chan) @@ -441,20 +460,26 @@ class Defender(IModule): return False await self.update_configuration(key, 0) + self.reputationTimer_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg( nick_from=dnickname, msg=f"[ {self.ctx.Config.COLORS.red}REPUTATION{self.ctx.Config.COLORS.black} ] : Deactivated by {fromuser}", channel=dchanlog ) + await self.ctx.Irc.Protocol.send2socket(f":{service_id} SAMODE {jail_chan} -{dumodes} {dnickname}") - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {jail_chan} -sS") - await self.ctx.Irc.Protocol.send2socket(f":{service_id} PART {jail_chan}") + await self.ctx.Irc.Protocol.send_set_mode('-sS', channel_name=jail_chan) + await self.ctx.Irc.Protocol.send_part_chan(service_id, jail_chan) for chan in self.ctx.Channel.UID_CHANNEL_DB: if chan.name != jail_chan: - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {chan.name} -b ~security-group:unknown-users") - await self.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {chan.name} -eee ~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users") + await self.ctx.Irc.Protocol.send_set_mode('-b', channel_name=chan.name, params='~security-group:unknown-users') + await self.ctx.Irc.Protocol.send_set_mode( + '-eee', + channel_name=chan.name, + params='~security-group:webirc-users ~security-group:known-users ~security-group:websocket-users' + ) await self.ctx.Channel.db_query_channel('del', self.module_name, jail_chan) @@ -464,12 +489,17 @@ class Defender(IModule): match get_options: case 'release': # .reputation release [nick] - p = await self.ctx.Irc.Protocol link = self.ctx.Config.SERVEUR_LINK jailed_salon = self.ctx.Config.SALON_JAIL welcome_salon = self.ctx.Config.SALON_LIBERER client_obj = self.ctx.User.get_user(str(cmd[2])) + if self.mod_config.reputation != 1: + await self.ctx.Irc.Protocol.send_notice(nick_from=dnickname, + nick_to=fromuser, + msg="The reputation system is not activated!") + return None + if client_obj is None: await self.ctx.Irc.Protocol.send_notice(nick_from=dnickname, nick_to=fromuser, @@ -658,6 +688,7 @@ class Defender(IModule): await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog) return None + self.ctx.Base.create_asynctask(self.Threads.coro_local_scan(self)) await self.update_configuration(option, 1) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog) @@ -667,6 +698,7 @@ class Defender(IModule): return None await self.update_configuration(option, 0) + self.localscan_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog) @@ -676,6 +708,7 @@ class Defender(IModule): await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog) return None + self.ctx.Base.create_asynctask(self.Threads.coro_psutil_scan(self)) await self.update_configuration(option, 1) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog) @@ -685,6 +718,7 @@ class Defender(IModule): return None await self.update_configuration(option, 0) + self.psutil_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog) @@ -694,6 +728,7 @@ class Defender(IModule): await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog) return None + self.ctx.Base.create_asynctask(self.Threads.coro_abuseipdb_scan(self)) await self.update_configuration(option, 1) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog) @@ -703,6 +738,7 @@ class Defender(IModule): return None await self.update_configuration(option, 0) + self.abuseipdb_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog) @@ -712,6 +748,7 @@ class Defender(IModule): await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog) return None + self.ctx.Base.create_asynctask(self.Threads.coro_freeipapi_scan(self)) await self.update_configuration(option, 1) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog) @@ -721,6 +758,7 @@ class Defender(IModule): return None await self.update_configuration(option, 0) + self.freeipapi_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog) @@ -730,6 +768,7 @@ class Defender(IModule): await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Already activated", channel=dchanlog) return None + self.ctx.Base.create_asynctask(self.Threads.coro_cloudfilt_scan(self)) await self.update_configuration(option, 1) await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_green}PROXY_SCAN {option.upper()}{color_black} ] : Activated by {fromuser}", channel=dchanlog) @@ -739,6 +778,7 @@ class Defender(IModule): return None await self.update_configuration(option, 0) + self.cloudfilt_isRunning = False await self.ctx.Irc.Protocol.send_priv_msg(nick_from=dnickname, msg=f"[ {color_red}PROXY_SCAN {option.upper()}{color_black} ] : Deactivated by {fromuser}", channel=dchanlog) @@ -915,3 +955,6 @@ class Defender(IModule): await self.join_saved_channels() return None + + case _: + pass \ No newline at end of file diff --git a/mods/defender/schemas.py b/mods/defender/schemas.py index a7d7a78..05fbfd1 100644 --- a/mods/defender/schemas.py +++ b/mods/defender/schemas.py @@ -28,9 +28,11 @@ class FloodUser(MainModel): nbr_msg: int = 0 first_msg_time: int = 0 -DB_FLOOD_USERS: list[FloodUser] = [] -DB_ABUSEIPDB_USERS: list[MUser] = [] -DB_FREEIPAPI_USERS: list[MUser] = [] -DB_CLOUDFILT_USERS: list[MUser] = [] -DB_PSUTIL_USERS: list[MUser] = [] -DB_LOCALSCAN_USERS: list[MUser] = [] \ No newline at end of file + +class RepDB: + DB_FLOOD_USERS: list[FloodUser] = [] + DB_ABUSEIPDB_USERS: list[MUser] = [] + DB_FREEIPAPI_USERS: list[MUser] = [] + DB_CLOUDFILT_USERS: list[MUser] = [] + DB_PSUTIL_USERS: list[MUser] = [] + DB_LOCALSCAN_USERS: list[MUser] = [] \ No newline at end of file diff --git a/mods/defender/threads.py b/mods/defender/threads.py index 6d171de..3382db8 100644 --- a/mods/defender/threads.py +++ b/mods/defender/threads.py @@ -1,89 +1,251 @@ import asyncio -from typing import TYPE_CHECKING -from time import sleep +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from mods.defender.mod_defender import Defender async def coro_apply_reputation_sanctions(uplink: 'Defender'): + uplink.reputationTimer_isRunning = True while uplink.reputationTimer_isRunning: await uplink.mod_utils.action_apply_reputation_santions(uplink) await asyncio.sleep(5) async def coro_cloudfilt_scan(uplink: 'Defender'): + uplink.cloudfilt_isRunning = True + service_id = uplink.ctx.Config.SERVICE_ID + service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG + color_red = uplink.ctx.Config.COLORS.red + nogc = uplink.ctx.Config.COLORS.nogc + nogc = uplink.ctx.Config.COLORS.nogc + p = uplink.ctx.Irc.Protocol while uplink.cloudfilt_isRunning: - list_to_remove:list = [] - for user in uplink.Schemas.DB_CLOUDFILT_USERS: - await uplink.mod_utils.action_scan_client_with_cloudfilt(uplink, user) - list_to_remove.append(user) - await asyncio.sleep(1) + try: + list_to_remove:list = [] + for user in uplink.Schemas.DB_CLOUDFILT_USERS: + if user.remote_ip not in uplink.ctx.Config.WHITELISTED_IP: + result: Optional[dict] = await uplink.ctx.Base.create_thread_io( + uplink.mod_utils.action_scan_client_with_cloudfilt, + uplink, user + ) + list_to_remove.append(user) - for user_model in list_to_remove: - uplink.Schemas.DB_CLOUDFILT_USERS.remove(user_model) + if not result: + continue - await asyncio.sleep(1) + remote_ip = user.remote_ip + fullname = f'{user.nickname}!{user.username}@{user.hostname}' + + r_host = result.get('host', None) + r_countryiso = result.get('countryiso', None) + r_listed = result.get('listed', False) + r_listedby = result.get('listed_by', None) + + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}CLOUDFILT_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Host: {r_host} | country: {r_countryiso} | listed: {r_listed} | listed by : {r_listedby}", + channel=service_chanlog) + + uplink.ctx.Logs.debug(f"[CLOUDFILT SCAN] ({fullname}) connected from ({r_countryiso}), Listed: {r_listed}, by: {r_listedby}") + + if r_listed: + await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} Your connexion is listed as dangerous {r_listed} {r_listedby} - detected by cloudfilt") + uplink.ctx.Logs.debug(f"[CLOUDFILT SCAN GLINE] Dangerous connection ({fullname}) from ({r_countryiso}) Listed: {r_listed}, by: {r_listedby}") + + + await asyncio.sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_CLOUDFILT_USERS.remove(user_model) + + await asyncio.sleep(1.5) + except ValueError as ve: + uplink.ctx.Logs.debug(f"The value to remove is not in the list. {ve}") + except TimeoutError as te: + uplink.ctx.Logs.debug(f"Timeout Error {te}") async def coro_freeipapi_scan(uplink: 'Defender'): - + uplink.freeipapi_isRunning = True + service_id = uplink.ctx.Config.SERVICE_ID + service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG + color_red = uplink.ctx.Config.COLORS.red + nogc = uplink.ctx.Config.COLORS.nogc + p = uplink.ctx.Irc.Protocol + while uplink.freeipapi_isRunning: + try: + list_to_remove: list = [] + for user in uplink.Schemas.DB_FREEIPAPI_USERS: + if user.remote_ip not in uplink.ctx.Config.WHITELISTED_IP: + result: Optional[dict] = await uplink.ctx.Base.create_thread_io( + uplink.mod_utils.action_scan_client_with_freeipapi, + uplink, user + ) - list_to_remove: list = [] - for user in uplink.Schemas.DB_FREEIPAPI_USERS: - await uplink.mod_utils.action_scan_client_with_freeipapi(uplink, user) - list_to_remove.append(user) - await asyncio.sleep(1) + if not result: + continue - for user_model in list_to_remove: - uplink.Schemas.DB_FREEIPAPI_USERS.remove(user_model) + # pseudo!ident@host + remote_ip = user.remote_ip + fullname = f'{user.nickname}!{user.username}@{user.hostname}' - await asyncio.sleep(1) + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}FREEIPAPI_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Proxy: {str(result['isProxy'])} | Country : {str(result['countryCode'])}", + channel=service_chanlog) + uplink.ctx.Logs.debug(f"[FREEIPAPI SCAN] ({fullname}) connected from ({result['countryCode']}), Proxy: {result['isProxy']}") + + if result['isProxy']: + await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} This server do not allow proxy connexions {str(result['isProxy'])} - detected by freeipapi") + uplink.ctx.Logs.debug(f"[FREEIPAPI SCAN GLINE] Server do not allow proxy connexions {result['isProxy']}") + + list_to_remove.append(user) + await asyncio.sleep(1) + + # remove users from the list + for user_model in list_to_remove: + uplink.Schemas.DB_FREEIPAPI_USERS.remove(user_model) + + await asyncio.sleep(1.5) + except ValueError as ve: + uplink.ctx.Logs.debug(f"The value to remove is not in the list. {ve}") + except TimeoutError as te: + uplink.ctx.Logs.debug(f"Timeout Error {te}") async def coro_abuseipdb_scan(uplink: 'Defender'): + uplink.abuseipdb_isRunning = True + service_id = uplink.ctx.Config.SERVICE_ID + service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG + color_red = uplink.ctx.Config.COLORS.red + nogc = uplink.ctx.Config.COLORS.nogc + p = uplink.ctx.Irc.Protocol + while uplink.abuseipdb_isRunning: + try: + list_to_remove: list = [] + for user in uplink.Schemas.DB_ABUSEIPDB_USERS: + if user.remote_ip not in uplink.ctx.Config.WHITELISTED_IP: - list_to_remove: list = [] - print(uplink.Schemas.DB_ABUSEIPDB_USERS) - for user in uplink.Schemas.DB_ABUSEIPDB_USERS: - await uplink.mod_utils.action_scan_client_with_abuseipdb(uplink, user) - list_to_remove.append(user) - await asyncio.sleep(1) + result: Optional[dict] = await uplink.ctx.Base.create_thread_io( + uplink.mod_utils.action_scan_client_with_abuseipdb, + uplink, user + ) + list_to_remove.append(user) - print(list_to_remove) - for user_model in list_to_remove: - uplink.Schemas.DB_ABUSEIPDB_USERS.remove(user_model) + if not result: + continue - await asyncio.sleep(1) + remote_ip = user.remote_ip + fullname = f'{user.nickname}!{user.username}@{user.hostname}' + + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}ABUSEIPDB_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}", + channel=service_chanlog + ) + uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN] ({fullname}) connected from ({result['country']}), Score: {result['score']}, Tor: {result['isTor']}") + + if result['isTor']: + await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} This server do not allow Tor connexions {str(result['isTor'])} - Detected by Abuseipdb") + uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not allow Tor connections Tor: {result['isTor']}, Score: {result['score']}") + elif result['score'] >= 95: + await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} You were banned from this server because your abuse score is = {str(result['score'])} - Detected by Abuseipdb") + uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not high risk connections Country: {result['country']}, Score: {result['score']}") + + await asyncio.sleep(1) + + for user_model in list_to_remove: + uplink.Schemas.DB_ABUSEIPDB_USERS.remove(user_model) + + await asyncio.sleep(1.5) + except ValueError as ve: + uplink.ctx.Logs.debug(f"The value to remove is not in the list. {ve}", exc_info=True) + except TimeoutError as te: + uplink.ctx.Logs.debug(f"Timeout Error {te}", exc_info=True) async def coro_local_scan(uplink: 'Defender'): + uplink.localscan_isRunning = True + service_id = uplink.ctx.Config.SERVICE_ID + service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG + color_red = uplink.ctx.Config.COLORS.red + nogc = uplink.ctx.Config.COLORS.nogc + p = uplink.ctx.Irc.Protocol while uplink.localscan_isRunning: - list_to_remove:list = [] - for user in uplink.Schemas.DB_LOCALSCAN_USERS: - await uplink.mod_utils.action_scan_client_with_local_socket(uplink, user) - list_to_remove.append(user) - await asyncio.sleep(1) + try: + list_to_remove:list = [] + for user in uplink.Schemas.DB_LOCALSCAN_USERS: + if user.remote_ip not in uplink.ctx.Config.WHITELISTED_IP: + list_to_remove.append(user) + result = await uplink.ctx.Base.create_thread_io( + uplink.mod_utils.action_scan_client_with_local_socket, + uplink, user + ) - for user_model in list_to_remove: - uplink.Schemas.DB_LOCALSCAN_USERS.remove(user_model) + if not result: + continue + + fullname = f'{user.nickname}!{user.username}@{user.hostname}' + opened_ports = result['opened_ports'] + closed_ports = result['closed_ports'] + if opened_ports: + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}LOCAL_SCAN{nogc} ] {fullname} ({user.remote_ip}) : The Port(s) {opened_ports} are opened on this remote ip [{user.remote_ip}]", + channel=service_chanlog + ) + if closed_ports: + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}LOCAL_SCAN{nogc} ] {fullname} ({user.remote_ip}) : The Port(s) {closed_ports} are closed on this remote ip [{user.remote_ip}]", + channel=service_chanlog + ) + + await asyncio.sleep(1) - await asyncio.sleep(1) + for user_model in list_to_remove: + uplink.Schemas.DB_LOCALSCAN_USERS.remove(user_model) + + await asyncio.sleep(1.5) + except ValueError as ve: + uplink.ctx.Logs.debug(f"The value to remove is not in the list. {ve}") + except TimeoutError as te: + uplink.ctx.Logs.debug(f"Timeout Error {te}") async def coro_psutil_scan(uplink: 'Defender'): + uplink.psutil_isRunning = True + service_id = uplink.ctx.Config.SERVICE_ID + service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG + color_red = uplink.ctx.Config.COLORS.red + nogc = uplink.ctx.Config.COLORS.nogc + p = uplink.ctx.Irc.Protocol - while uplink.psutil_isRunning: - + while uplink.psutil_isRunning: + try: list_to_remove:list = [] for user in uplink.Schemas.DB_PSUTIL_USERS: - await uplink.mod_utils.action_scan_client_with_psutil(uplink, user) + result = await uplink.ctx.Base.create_thread_io(uplink.mod_utils.action_scan_client_with_psutil, uplink, user) list_to_remove.append(user) + if not result: + continue + + fullname = f'{user.nickname}!{user.username}@{user.hostname}' + await p.send_priv_msg( + nick_from=service_id, + msg=f"[ {color_red}PSUTIL_SCAN{nogc} ] {fullname} ({user.remote_ip}) is using ports {result}", + channel=service_chanlog + ) await asyncio.sleep(1) for user_model in list_to_remove: uplink.Schemas.DB_PSUTIL_USERS.remove(user_model) - await asyncio.sleep(1) + await asyncio.sleep(1.5) + except ValueError as ve: + uplink.ctx.Logs.debug(f"The value to remove is not in the list. {ve}") + except TimeoutError as te: + uplink.ctx.Logs.debug(f"Timeout Error {te}") async def coro_autolimit(uplink: 'Defender'): @@ -104,7 +266,6 @@ async def coro_autolimit(uplink: 'Defender'): chan_list: list[str] = [c.name for c in uplink.ctx.Channel.UID_CHANNEL_DB] while uplink.autolimit_isRunning: - if uplink.mod_config.autolimit == 0: uplink.ctx.Logs.debug("autolimit deactivated ... stopping the current thread") break @@ -112,7 +273,7 @@ async def coro_autolimit(uplink: 'Defender'): for chan in uplink.ctx.Channel.UID_CHANNEL_DB: for chan_copy in chanObj_copy: if chan_copy["name"] == chan.name and len(chan.uids) != chan_copy["uids_count"]: - await p.send2socket(f":{uplink.ctx.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.mod_config.autolimit_amount}") + await p.send_set_mode('+l', channel_name=chan.name, params=len(chan.uids) + uplink.mod_config.autolimit_amount) chan_copy["uids_count"] = len(chan.uids) if chan.name not in chan_list: @@ -128,13 +289,13 @@ async def coro_autolimit(uplink: 'Defender'): # Si c'est la premiere execution if INIT == 1: for chan in uplink.ctx.Channel.UID_CHANNEL_DB: - await p.send2socket(f":{uplink.ctx.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.mod_config.autolimit_amount}") + await p.send_set_mode('+l', channel_name=chan.name, params=len(chan.uids) + uplink.mod_config.autolimit_amount) # Si le nouveau amount est différent de l'initial if init_amount != uplink.mod_config.autolimit_amount: init_amount = uplink.mod_config.autolimit_amount for chan in uplink.ctx.Channel.UID_CHANNEL_DB: - await p.send2socket(f":{uplink.ctx.Config.SERVICE_ID} MODE {chan.name} +l {len(chan.uids) + uplink.mod_config.autolimit_amount}") + await p.send_set_mode('+l', channel_name=chan.name, params=len(chan.uids) + uplink.mod_config.autolimit_amount) INIT = 0 @@ -142,7 +303,6 @@ async def coro_autolimit(uplink: 'Defender'): await asyncio.sleep(uplink.mod_config.autolimit_interval) for chan in uplink.ctx.Channel.UID_CHANNEL_DB: - # await p.send2socket(f":{uplink.ctx.Config.SERVICE_ID} MODE {chan.name} -l") await p.send_set_mode('-l', channel_name=chan.name) uplink.ctx.Irc.autolimit_started = False @@ -158,7 +318,6 @@ async def coro_release_mode_mute(uplink: 'Defender', action: str, channel: str): channel (str): The related channel """ - service_id = uplink.ctx.Config.SERVICE_ID timeout = uplink.mod_config.flood_timer await asyncio.sleep(timeout) @@ -169,6 +328,6 @@ async def coro_release_mode_mute(uplink: 'Defender', action: str, channel: str): match action: case 'mode-m': # Action -m sur le salon - await uplink.ctx.Irc.Protocol.send2socket(f":{service_id} MODE {channel} -m") + await uplink.ctx.Irc.Protocol.send_set_mode('-m', channel_name=channel) case _: pass diff --git a/mods/defender/utils.py b/mods/defender/utils.py index a880a42..a6e48b7 100644 --- a/mods/defender/utils.py +++ b/mods/defender/utils.py @@ -267,6 +267,7 @@ async def handle_on_uid(uplink: 'Defender', srvmsg: list[str]): #################### # [:] UID []+ : # [:] UID nickname hopcount timestamp username hostname uid servicestamp umodes virthost cloakedhost ip :gecos + async def action_on_flood(uplink: 'Defender', srvmsg: list[str]): confmodel = uplink.mod_config @@ -316,17 +317,16 @@ async def action_on_flood(uplink: 'Defender', srvmsg: list[str]): fu.nbr_msg = 0 get_diff_secondes = unixtime - fu.first_msg_time elif fu.nbr_msg > flood_message: - uplink.ctx.Logs.info('system de flood detecté') + await p.send_set_mode('+m', channel_name=channel) await p.send_priv_msg( nick_from=dnickname, msg=f"{color_red} {color_bold} Flood detected. Apply the +m mode (Ô_o)", channel=channel ) - await p.send2socket(f":{service_id} MODE {channel} +m") - uplink.ctx.Logs.info(f'FLOOD Détecté sur {get_detected_nickname} mode +m appliqué sur le salon {channel}') + uplink.ctx.Logs.debug(f'[FLOOD] {get_detected_nickname} triggered +m mode on the channel {channel}') fu.nbr_msg = 0 fu.first_msg_time = unixtime - uplink.ctx.Base.create_asynctask(dthreads.coro_release_mode_mute(uplink, 'mode-m', channel)) + uplink.ctx.Base.create_asynctask(uplink.Threads.coro_release_mode_mute(uplink, 'mode-m', channel)) async def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ): @@ -336,10 +336,14 @@ async def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ): confmodel = uplink.mod_config get_reputation = uplink.ctx.Reputation.get_reputation(jailed_uid) - if get_reputation is None: uplink.ctx.Logs.warning(f'UID {jailed_uid} has not been found') - return + return None + + if get_reputation.isWebirc or get_reputation.isWebsocket: + uplink.ctx.Logs.debug(f'This nickname is exampted from the reputation system (Webirc or Websocket). {get_reputation.nickname} ({get_reputation.uid})') + uplink.ctx.Reputation.delete(get_reputation.uid) + return None salon_logs = gconfig.SERVICE_CHANLOG salon_jail = gconfig.SALON_JAIL @@ -360,7 +364,7 @@ async def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ): # Si le user ne vient pas de webIrc await p.send_sajoin(nick_to_sajoin=jailed_nickname, channel_name=salon_jail) await p.send_priv_msg(nick_from=gconfig.SERVICE_NICKNAME, - msg=f" [{color_red} REPUTATION {nogc}] : Connexion de {jailed_nickname} ({jailed_score}) ==> {salon_jail}", + msg=f" [ {color_red}REPUTATION{nogc} ]: The nickname {jailed_nickname} has been sent to {salon_jail} because his reputation score is ({jailed_score})", channel=salon_logs ) await p.send_notice( @@ -371,7 +375,7 @@ async def action_add_reputation_sanctions(uplink: 'Defender', jailed_uid: str ): if reputation_ban_all_chan == 1: for chan in uplink.ctx.Channel.UID_CHANNEL_DB: if chan.name != salon_jail: - await p.send2socket(f":{service_id} MODE {chan.name} +b {jailed_nickname}!*@*") + await p.send_set_mode('+b', channel_name=chan.name, params=f'{jailed_nickname}!*@*') await p.send2socket(f":{service_id} KICK {chan.name} {jailed_nickname}") uplink.ctx.Logs.info(f"[REPUTATION] {jailed_nickname} jailed (UID: {jailed_uid}, score: {jailed_score})") @@ -419,14 +423,14 @@ async def action_apply_reputation_santions(uplink: 'Defender') -> None: for chan in uplink.ctx.Channel.UID_CHANNEL_DB: if chan.name != salon_jail and ban_all_chan == 1: get_user_reputation = uplink.ctx.Reputation.get_reputation(uid) - await p.send2socket(f":{service_id} MODE {chan.name} -b {get_user_reputation.nickname}!*@*") + await p.send_set_mode('-b', channel_name=chan.name, params=f"{get_user_reputation.nickname}!*@*") # Lorsqu'un utilisateur quitte, il doit être supprimé de {UID_DB}. uplink.ctx.Channel.delete_user_from_all_channel(uid) uplink.ctx.Reputation.delete(uid) uplink.ctx.User.delete(uid) -async def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: +def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: """Analyse l'ip avec cloudfilt Cette methode devra etre lancer toujours via un thread ou un timer. Args: @@ -438,11 +442,6 @@ async def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUs """ remote_ip = user_model.remote_ip - username = user_model.username - hostname = user_model.hostname - nickname = user_model.nickname - p = uplink.ctx.Irc.Protocol - if remote_ip in uplink.ctx.Config.WHITELISTED_IP: return None if uplink.mod_config.cloudfilt_scan == 0: @@ -450,52 +449,28 @@ async def action_scan_client_with_cloudfilt(uplink: 'Defender', user_model: 'MUs if uplink.cloudfilt_key == '': return None - service_id = uplink.ctx.Config.SERVICE_ID - service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG - color_red = uplink.ctx.Config.COLORS.red - nogc = uplink.ctx.Config.COLORS.nogc - url = "https://developers18334.cloudfilt.com/" + data = {'ip': remote_ip, 'key': uplink.cloudfilt_key} + with requests.Session() as sess: + response = sess.post(url=url, data=data) - data = { - 'ip': remote_ip, - 'key': uplink.cloudfilt_key - } + # Formatted output + decoded_response: dict = loads(response.text) + status_code = response.status_code + if status_code != 200: + uplink.ctx.Logs.warning(f'Error connecting to cloudfilt API | Code: {str(status_code)}') + return - response = requests.post(url=url, data=data) - # Formatted output - decoded_response: dict = loads(response.text) - status_code = response.status_code - if status_code != 200: - uplink.ctx.Logs.warning(f'Error connecting to cloudfilt API | Code: {str(status_code)}') - return + result = { + 'countryiso': decoded_response.get('countryiso', None), + 'listed': decoded_response.get('listed', False), + 'listed_by': decoded_response.get('listed_by', None), + 'host': decoded_response.get('host', None) + } - result = { - 'countryiso': decoded_response.get('countryiso', None), - 'listed': decoded_response.get('listed', None), - 'listed_by': decoded_response.get('listed_by', None), - 'host': decoded_response.get('host', None) - } + return result - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - await p.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}CLOUDFILT_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Host: {str(result['host'])} | country: {str(result['countryiso'])} | listed: {str(result['listed'])} | listed by : {str(result['listed_by'])}", - channel=service_chanlog) - - uplink.ctx.Logs.debug(f"[CLOUDFILT SCAN] ({fullname}) connected from ({result['countryiso']}), Listed: {result['listed']}, by: {result['listed_by']}") - - if result['listed']: - await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} Your connexion is listed as dangerous {str(result['listed'])} {str(result['listed_by'])} - detected by cloudfilt") - uplink.ctx.Logs.debug(f"[CLOUDFILT SCAN GLINE] Dangerous connection ({fullname}) from ({result['countryiso']}) Listed: {result['listed']}, by: {result['listed_by']}") - - response.close() - - return result - -async def action_scan_client_with_freeipapi(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: +def action_scan_client_with_freeipapi(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: """Analyse l'ip avec Freeipapi Cette methode devra etre lancer toujours via un thread ou un timer. Args: @@ -505,64 +480,35 @@ async def action_scan_client_with_freeipapi(uplink: 'Defender', user_model: 'MUs dict[str, any] | None: les informations du provider keys : 'countryCode', 'isProxy' """ - p = uplink.ctx.Irc.Protocol remote_ip = user_model.remote_ip - username = user_model.username - hostname = user_model.hostname - nickname = user_model.nickname - if remote_ip in uplink.ctx.Config.WHITELISTED_IP: return None if uplink.mod_config.freeipapi_scan == 0: return None - service_id = uplink.ctx.Config.SERVICE_ID - service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG - color_red = uplink.ctx.Config.COLORS.red - nogc = uplink.ctx.Config.COLORS.nogc + with requests.Session() as sess: + url = f'https://freeipapi.com/api/json/{remote_ip}' + headers = {'Accept': 'application/json'} + response = sess.request(method='GET', url=url, headers=headers, timeout=uplink.timeout) - url = f'https://freeipapi.com/api/json/{remote_ip}' + # Formatted output + decoded_response: dict = loads(response.text) - headers = { - 'Accept': 'application/json', - } + status_code = response.status_code + if status_code == 429: + uplink.ctx.Logs.warning('Too Many Requests - The rate limit for the API has been exceeded.') + return None + elif status_code != 200: + uplink.ctx.Logs.warning(f'status code = {str(status_code)}') + return None - response = requests.request(method='GET', url=url, headers=headers, timeout=uplink.timeout) + result = { + 'countryCode': decoded_response.get('countryCode', None), + 'isProxy': decoded_response.get('isProxy', None) + } + return result - # Formatted output - decoded_response: dict = loads(response.text) - - status_code = response.status_code - if status_code == 429: - uplink.ctx.Logs.warning('Too Many Requests - The rate limit for the API has been exceeded.') - return None - elif status_code != 200: - uplink.ctx.Logs.warning(f'status code = {str(status_code)}') - return None - - result = { - 'countryCode': decoded_response.get('countryCode', None), - 'isProxy': decoded_response.get('isProxy', None) - } - - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - await p.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}FREEIPAPI_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Proxy: {str(result['isProxy'])} | Country : {str(result['countryCode'])}", - channel=service_chanlog) - uplink.ctx.Logs.debug(f"[FREEIPAPI SCAN] ({fullname}) connected from ({result['countryCode']}), Proxy: {result['isProxy']}") - - if result['isProxy']: - await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} This server do not allow proxy connexions {str(result['isProxy'])} - detected by freeipapi") - uplink.ctx.Logs.debug(f"[FREEIPAPI SCAN GLINE] Server do not allow proxy connexions {result['isProxy']}") - - response.close() - - return result - -async def action_scan_client_with_abuseipdb(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: +def action_scan_client_with_abuseipdb(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: """Analyse l'ip avec AbuseIpDB Cette methode devra etre lancer toujours via un thread ou un timer. Args: @@ -572,121 +518,81 @@ async def action_scan_client_with_abuseipdb(uplink: 'Defender', user_model: 'MUs Returns: dict[str, str] | None: les informations du provider """ - p = uplink.ctx.Irc.Protocol remote_ip = user_model.remote_ip - username = user_model.username - hostname = user_model.hostname - nickname = user_model.nickname if remote_ip in uplink.ctx.Config.WHITELISTED_IP: return None if uplink.mod_config.abuseipdb_scan == 0: return None - if uplink.abuseipdb_key == '': return None - url = 'https://api.abuseipdb.com/api/v2/check' - querystring = { - 'ipAddress': remote_ip, - 'maxAgeInDays': '90' - } + with requests.Session() as sess: + url = 'https://api.abuseipdb.com/api/v2/check' + querystring = {'ipAddress': remote_ip, 'maxAgeInDays': '90'} + headers = { + 'Accept': 'application/json', + 'Key': uplink.abuseipdb_key + } - headers = { - 'Accept': 'application/json', - 'Key': uplink.abuseipdb_key - } + response = sess.request(method='GET', url=url, headers=headers, params=querystring, timeout=uplink.timeout) - response = requests.request(method='GET', url=url, headers=headers, params=querystring, timeout=uplink.timeout) + if response.status_code != 200: + uplink.ctx.Logs.warning(f'status code = {str(response.status_code)}') + return None - # Formatted output - decoded_response: dict[str, dict] = loads(response.text) + # Formatted output + decoded_response: dict[str, dict] = loads(response.text) - if 'data' not in decoded_response: - return None + if 'data' not in decoded_response: + return None - result = { - 'score': decoded_response.get('data', {}).get('abuseConfidenceScore', 0), - 'country': decoded_response.get('data', {}).get('countryCode', None), - 'isTor': decoded_response.get('data', {}).get('isTor', None), - 'totalReports': decoded_response.get('data', {}).get('totalReports', 0) - } - - service_id = uplink.ctx.Config.SERVICE_ID - service_chanlog = uplink.ctx.Config.SERVICE_CHANLOG - color_red = uplink.ctx.Config.COLORS.red - nogc = uplink.ctx.Config.COLORS.nogc - - # pseudo!ident@host - fullname = f'{nickname}!{username}@{hostname}' - - await p.send_priv_msg( - nick_from=service_id, - msg=f"[ {color_red}ABUSEIPDB_SCAN{nogc} ] : Connexion de {fullname} ({remote_ip}) ==> Score: {str(result['score'])} | Country : {result['country']} | Tor : {str(result['isTor'])} | Total Reports : {str(result['totalReports'])}", - channel=service_chanlog - ) - uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN] ({fullname}) connected from ({result['country']}), Score: {result['score']}, Tor: {result['isTor']}") - - if result['isTor']: - await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} This server do not allow Tor connexions {str(result['isTor'])} - Detected by Abuseipdb") - uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not allow Tor connections Tor: {result['isTor']}, Score: {result['score']}") - elif result['score'] >= 95: - await p.send2socket(f":{service_id} GLINE +*@{remote_ip} {uplink.ctx.Config.GLINE_DURATION} You were banned from this server because your abuse score is = {str(result['score'])} - Detected by Abuseipdb") - uplink.ctx.Logs.debug(f"[ABUSEIPDB SCAN GLINE] Server do not high risk connections Country: {result['country']}, Score: {result['score']}") - - response.close() + result = { + 'score': decoded_response.get('data', {}).get('abuseConfidenceScore', 0), + 'country': decoded_response.get('data', {}).get('countryCode', None), + 'isTor': decoded_response.get('data', {}).get('isTor', None), + 'totalReports': decoded_response.get('data', {}).get('totalReports', 0) + } return result -async def action_scan_client_with_local_socket(uplink: 'Defender', user_model: 'MUser'): +def action_scan_client_with_local_socket(uplink: 'Defender', user_model: 'MUser') -> Optional[dict[str, str]]: """local_scan Args: uplink (Defender): Defender instance object user_model (MUser): l'objet User qui contient l'ip """ - p = uplink.ctx.Irc.Protocol remote_ip = user_model.remote_ip - username = user_model.username - hostname = user_model.hostname - nickname = user_model.nickname - fullname = f'{nickname}!{username}@{hostname}' - if remote_ip in uplink.ctx.Config.WHITELISTED_IP: return None + result = {'opened_ports': [], 'closed_ports': []} + for port in uplink.ctx.Config.PORTS_TO_SCAN: - try: - newSocket = '' - newSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK) - newSocket.settimeout(0.5) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM or socket.SOCK_NONBLOCK) as sock: + try: + sock.settimeout(0.5) + connection = (remote_ip, uplink.ctx.Base.int_if_possible(port)) + sock.connect(connection) - connection = (remote_ip, uplink.ctx.Base.int_if_possible(port)) - newSocket.connect(connection) + result['opened_ports'].append(port) + uplink.ctx.Base.running_sockets.append(sock) + sock.shutdown(socket.SHUT_RDWR) + uplink.ctx.Base.running_sockets.remove(sock) + return result - await p.send_priv_msg( - nick_from=uplink.ctx.Config.SERVICE_NICKNAME, - msg=f"[ {uplink.ctx.Config.COLORS.red}PROXY_SCAN{uplink.ctx.Config.COLORS.nogc} ] {fullname} ({remote_ip}) : Port [{str(port)}] ouvert sur l'adresse ip [{remote_ip}]", - channel=uplink.ctx.Config.SERVICE_CHANLOG - ) - # print(f"=======> Le port {str(port)} est ouvert !!") - uplink.ctx.Base.running_sockets.append(newSocket) - # print(newSocket) - newSocket.shutdown(socket.SHUT_RDWR) - newSocket.close() + except (socket.timeout, ConnectionRefusedError): + uplink.ctx.Logs.debug(f"[LOCAL SCAN] Port {remote_ip}:{str(port)} is close.") + result['closed_ports'].append(port) + except AttributeError as ae: + uplink.ctx.Logs.warning(f"AttributeError ({remote_ip}): {ae}") + except socket.gaierror as err: + uplink.ctx.Logs.warning(f"Address Info Error ({remote_ip}): {err}") - except (socket.timeout, ConnectionRefusedError): - uplink.ctx.Logs.info(f"Le port {remote_ip}:{str(port)} est fermé") - except AttributeError as ae: - uplink.ctx.Logs.warning(f"AttributeError ({remote_ip}): {ae}") - except socket.gaierror as err: - uplink.ctx.Logs.warning(f"Address Info Error ({remote_ip}): {err}") - finally: - # newSocket.shutdown(socket.SHUT_RDWR) - newSocket.close() - uplink.ctx.Logs.info('=======> Fermeture de la socket') + return result -async def action_scan_client_with_psutil(uplink: 'Defender', user_model: 'MUser') -> list[int]: +def action_scan_client_with_psutil(uplink: 'Defender', user_model: 'MUser') -> list[int]: """psutil_scan for Linux (should be run on the same location as the unrealircd server) Args: @@ -695,28 +601,16 @@ async def action_scan_client_with_psutil(uplink: 'Defender', user_model: 'MUser' Returns: list[int]: list of ports """ - p = uplink.ctx.Irc.Protocol remote_ip = user_model.remote_ip - username = user_model.username - hostname = user_model.hostname - nickname = user_model.nickname - if remote_ip in uplink.ctx.Config.WHITELISTED_IP: return None + if uplink.mod_config.psutil_scan == 0: + return None try: connections = psutil.net_connections(kind='inet') - fullname = f'{nickname}!{username}@{hostname}' - matching_ports = [conn.raddr.port for conn in connections if conn.raddr and conn.raddr.ip == remote_ip] - uplink.ctx.Logs.info(f"Connexion of {fullname} ({remote_ip}) using ports : {str(matching_ports)}") - - if matching_ports: - await p.send_priv_msg( - nick_from=uplink.ctx.Config.SERVICE_NICKNAME, - msg=f"[ {uplink.ctx.Config.COLORS.red}PSUTIL_SCAN{uplink.ctx.Config.COLORS.black} ] {fullname} ({remote_ip}) : is using ports {matching_ports}", - channel=uplink.ctx.Config.SERVICE_CHANLOG - ) + uplink.ctx.Logs.debug(f"Connexion of ({remote_ip}) using ports : {str(matching_ports)}") return matching_ports diff --git a/mods/jsonrpc/threads.py b/mods/jsonrpc/threads.py index c28442e..df57633 100644 --- a/mods/jsonrpc/threads.py +++ b/mods/jsonrpc/threads.py @@ -1,4 +1,3 @@ -import asyncio from typing import TYPE_CHECKING if TYPE_CHECKING: