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] 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: