diff --git a/.gitignore b/.gitignore index c3c8df7..67996c7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ db/ logs/ __pycache__/ configuration.json +install.log test.py \ No newline at end of file diff --git a/README.md b/README.md index 1d1c647..5dee40c 100644 --- a/README.md +++ b/README.md @@ -21,35 +21,85 @@ Il permet aux opérateurs de gérer efficacement un canal, tout en offrant aux u Kick: Les utilisateurs peuvent voter pour expulser un membre du canal. Autres actions: Possibilité d'étendre le système de vote à d'autres actions (ban, etc.). -# Installation et utilisation - Prérequis: - - Python version >= 3.10 - - Pip de python installé sur la machine - - Python librairies psutil & sqlalchemy & requests - - IRC Serveur Version >= UnrealIRCd-6.1.2.2 +# Installation automatique sur une machine Debian/Ubuntu - Installation: +Prérequis: + - Système d'exploitation Linux (Windows non supporté) + - Droits d'administrateur (root) pour l'exécution du script + - Python version 3.10 ou supérieure - Cloner le dépôt: - Bash - git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git - Utilisez ce code avec précaution. +Bash + $ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git + - Renommer le fichier exemple_configuration.json en configuration.json + - Configurer le fichier configuration.json + $ sudo python3 install.py - Configuration (configuration.json): - Le fichier configuration.json permet de personnaliser le service: - Serveur IRC: Adresse du serveur IRC. - Port: Port du serveur IRC. - Canal: Canal auquel se connecter. - Nom du Service: Nom d'utilisateur du bot sur le serveur. - Mot de passe: Mot de passe du link (si nécessaire). - Préfixes de commandes: Caractères utilisés pour déclencher les commandes. - Et bien d'autres... +Si votre configuration est bonne, votre service est censé etre connecté a votre réseau IRC - Extension: - Le code est modulaire et conçu pour être facilement étendu. Vous pouvez ajouter de nouvelles commandes, de nouvelles fonctionnalités (mods/mod_test.py est un exemple pour bien demarrer la création de son module). +# Installation manuelle: +Bash + $ git clone https://github.com/adator85/IRC_DEFENDER_MODULES.git + $ cd IRC_DEFENDER_MODULES + $ python3 -m venv .pyenv + $ source .pyenv/bin/activate + - Créer un service nommé "Defender.service" pour votre service et placer le dans "/etc/systemd/system/" + $ sudo systemctl start Defender - Contributions: +# Configuration + + SERVEUR (Serveur) + SERVEUR_IP: Adresse IP du serveur IRC à rejoindre. + SERVEUR_HOSTNAME: Nom d'hôte du serveur IRC à rejoindre (optionnel). + SERVEUR_LINK: Lien vers le serveur IRC (optionnel). + SERVEUR_PORT: Port de connexion au serveur IRC. + SERVEUR_PASSWORD: Mot de passe d'enregistrement du service sur le serveur IRC. + SERVEUR_ID: Identifiant unique du service. + SERVEUR_SSL: Active la connexion SSL sécurisée au serveur IRC (true/false). + SERVICE (Service) + SERVICE_NAME: Nom du service IRC. + SERVICE_NICKNAME: Surnom utilisé par le service sur le serveur IRC. + SERVICE_REALNAME: Nom réel du service affiché sur le serveur IRC. + SERVICE_USERNAME: Nom d'utilisateur utilisé par le service pour se connecter au serveur IRC. + SERVICE_HOST: Nom d'hôte du service affiché sur le serveur IRC (optionnel). + SERVICE_INFO: Description du service. + SERVICE_CHANLOG: Canal utilisé pour la journalisation des actions du service. + SERVICE_SMODES: Modes serveur appliqués aux canaux rejoints par le service. + SERVICE_CMODES: Modes de canal appliqués aux canaux rejoints par le service. + SERVICE_UMODES: Modes utilisateur appliqués au service. + SERVICE_PREFIX: Caractère utilisé comme préfixe des commandes du service. + COMPTE (Compte) + OWNER: Nom d'utilisateur possédant les droits d'administration du service. + PASSWORD: Mot de passe de l'administrateur du service. + CANAUX (Canaux) + SALON_JAIL: Canal utilisé comme prison pour les utilisateurs sanctionnés. + SALON_JAIL_MODES: Modes appliqués au canal de prison. + SALON_LIBERER: Canal utilisé pour la libération des utilisateurs sanctionnés. + API (API) + API_TIMEOUT: Durée maximale d'attente d'une réponse de l'API en secondes. + SCANNER (Scanner) + PORTS_TO_SCAN: Liste des ports à scanner pour détecter des serveurs potentiellement malveillants. + SÉCURITÉ (Sécurité) + WHITELISTED_IP: Liste d'adresses IP autorisées à contourner certaines restrictions. + GLINE_DURATION: Durée de bannissement temporaire d'un utilisateur en minutes. + DEBUG (Debug) + DEBUG_LEVEL: Niveau de verbosité des messages de debug (plus grand est le nombre, plus il y a d'informations). + COULEURS (Couleurs) + CONFIG_COLOR: Dictionnaire contenant des codes de couleurs IRC pour un meilleur affichage des messages. + + Modification de la configuration + + Vous devez modifier le fichier config.json en remplaçant les valeurs par défaut avec vos propres informations. Assurez-vous de bien lire la description de chaque paramètre pour une configuration optimale du service. + + Attention + + Le mot de passe de l'administrateur et le mot de passe du service doivent être modifiés pour des raisons de sécurité. + Ne partagez pas vos informations de connexion au serveur IRC avec des tiers. + +#Extension: + Le code est modulaire et conçu pour être facilement étendu. Vous pouvez ajouter de nouvelles commandes, de nouvelles fonctionnalités (mods/mod_test.py est un exemple pour bien demarrer la création de son module). + +# Contributions: Les contributions sont les bienvenues ! N'hésitez pas à ouvrir des issues ou des pull requests. - Avertissement: +# Avertissement: Ce bot est fourni "tel quel" sans aucune garantie. Utilisez-le à vos risques et périls. \ No newline at end of file diff --git a/core/Model.py b/core/Model.py index e5347d6..6769a52 100644 --- a/core/Model.py +++ b/core/Model.py @@ -38,8 +38,10 @@ class User: for record in self.UID_DB: if record.uid == newUser.uid: + # If the user exist then return False and do not go further exist = True self.log.debug(f'{record.uid} already exist') + return result if not exist: self.UID_DB.append(newUser) @@ -65,9 +67,11 @@ class User: for record in self.UID_DB: if record.uid == uid: + # If the user exist then update and return True and do not go further record.nickname = newNickname result = True self.log.debug(f'UID ({record.uid}) has been updated with new nickname {newNickname}') + return result if not result: self.log.critical(f'The new nickname {newNickname} was not updated, uid = {uid}') @@ -87,9 +91,11 @@ class User: for record in self.UID_DB: if record.uid == uid: + # If the user exist then remove and return True and do not go further self.UID_DB.remove(record) result = True self.log.debug(f'UID ({record.uid}) has been deleted') + return result if not result: self.log.critical(f'The UID {uid} was not deleted') @@ -179,8 +185,10 @@ class Admin: for record in self.UID_ADMIN_DB: if record.uid == newAdmin.uid: + # If the admin exist then return False and do not go further exist = True self.log.debug(f'{record.uid} already exist') + return result if not exist: self.UID_ADMIN_DB.append(newAdmin) @@ -191,37 +199,41 @@ class Admin: self.log.critical(f'The User Object was not inserted {newAdmin}') return result - + def update(self, uid: str, newNickname: str) -> bool: result = False for record in self.UID_ADMIN_DB: if record.uid == uid: + # If the admin exist, update and do not go further record.nickname = newNickname result = True self.log.debug(f'UID ({record.uid}) has been updated with new nickname {newNickname}') + return result if not result: self.log.critical(f'The new nickname {newNickname} was not updated, uid = {uid}') return result - + def delete(self, uid: str) -> bool: result = False for record in self.UID_ADMIN_DB: if record.uid == uid: + # If the admin exist, delete and do not go further self.UID_ADMIN_DB.remove(record) result = True self.log.debug(f'UID ({record.uid}) has been created') + return result if not result: self.log.critical(f'The UID {uid} was not deleted') return result - + def get_Admin(self, uidornickname: str) -> Union[AdminModel, None]: Admin = None @@ -273,15 +285,23 @@ class Channel: pass def insert(self, newChan: ChannelModel) -> bool: + """This method will insert a new channel and if the channel exist it will update the user list (uids) + Args: + newChan (ChannelModel): The channel model object + + Returns: + bool: True if new channel, False if channel exist (However UID could be updated) + """ result = False exist = False for record in self.UID_CHANNEL_DB: if record.name == newChan.name: + # If the channel exist, update the user list and do not go further exist = True self.log.debug(f'{record.name} already exist') - + for user in newChan.uids: record.uids.append(user) @@ -289,9 +309,11 @@ class Channel: del_duplicates = list(set(record.uids)) record.uids = del_duplicates self.log.debug(f'Updating a new UID to the channel {record}') + return result if not exist: + # If the channel don't exist, then create it self.UID_CHANNEL_DB.append(newChan) result = True self.log.debug(f'New Channel Created: ({newChan})') @@ -307,9 +329,12 @@ class Channel: for record in self.UID_CHANNEL_DB: if record.name == name: + # If the channel exist, then remove it and return True. + # As soon as the channel found, return True and stop the loop self.UID_CHANNEL_DB.remove(record) result = True self.log.debug(f'Channel ({record.name}) has been created') + return result if not result: self.log.critical(f'The Channel {name} was not deleted') @@ -325,13 +350,13 @@ class Channel: for user_id in record.uids: if self.Base.clean_uid(user_id) == uid: record.uids.remove(user_id) - self.log.debug(f'uid {uid} has been removed, here is the new object: {record}') + self.log.debug(f'The UID {uid} has been removed, here is the new object: {record}') result = True for record in self.UID_CHANNEL_DB: if not record.uids: self.UID_CHANNEL_DB.remove(record) - self.log.debug(f'Channel {record.name} has been removed, here is the new object: {record}') + self.log.debug(f'The Channel {record.name} has been removed, here is the new object: {record}') return result except ValueError as ve: @@ -347,13 +372,3 @@ class Channel: self.log.debug(f'Search {name} -- result = {Channel}') return Channel - - def get_mode(self, name:str) -> Union[str, None]: - - mode = None - for record in self.UID_CHANNEL_DB: - if record.name == name: - mode = record.mode - - self.log.debug(f'The mode of the channel {name} has been found: {mode}') - return mode diff --git a/core/base.py b/core/base.py index 19d9340..c3b4245 100644 --- a/core/base.py +++ b/core/base.py @@ -312,7 +312,7 @@ class Base: if thread.getName() != 'heartbeat': if not thread.is_alive(): self.running_threads.remove(thread) - self.logs.debug(f"Thread {str(thread.getName())} {str(thread.native_id)} removed") + self.logs.info(f"Thread {str(thread.getName())} {str(thread.native_id)} removed") # print(threading.enumerate()) except AssertionError as ae: diff --git a/core/installation.py b/core/installation.py index 8eb709b..e50b008 100644 --- a/core/installation.py +++ b/core/installation.py @@ -1,12 +1,16 @@ from importlib.util import find_spec -from subprocess import check_call, run +from subprocess import check_call, run, CalledProcessError from platform import python_version from sys import exit +import os class Install: def __init__(self) -> None: self.PYTHON_MIN_VERSION = '3.10' + + self.venv_folder_name = '.pyenv' + self.cmd_venv_command = ['python3', '-m', 'venv', self.venv_folder_name] self.module_to_install = ['sqlalchemy','psutil','requests'] if not self.checkPythonVersion(): @@ -38,6 +42,17 @@ class Install: return True + def run_subprocess(self, command:list) -> None: + + print(command) + try: + check_call(command) + print("La commande s'est terminée avec succès.") + except CalledProcessError as e: + print(f"La commande a échoué avec le code de retour :{e.returncode}") + print(f"Try to install dependencies ...") + exit(5) + def checkDependencies(self) -> None: """### Verifie les dépendances si elles sont installées - Test si les modules sont installés @@ -46,6 +61,11 @@ class Install: """ do_install = False + # Check if virtual env exist + if not os.path.exists(f'{self.venv_folder_name}'): + self.run_subprocess(self.cmd_venv_command) + do_install = True + for module in self.module_to_install: if find_spec(module) is None: do_install = True @@ -70,3 +90,10 @@ class Install: print(f"====> Module {module} installé") else: print(f"==> {module} already installed") + + print(f"#"*12) + print("Installation complete ...") + print("You must change environment using the command below") + print(f"source {self.venv_folder_name}{os.sep}bin{os.sep}activate") + print(f"#"*12) + exit(1) \ No newline at end of file diff --git a/core/irc.py b/core/irc.py index a472fdf..22dc206 100644 --- a/core/irc.py +++ b/core/irc.py @@ -803,10 +803,10 @@ class Irc: get_uid_or_nickname = str(cmd[0].replace(':','')) if len(cmd) == 6: - if cmd[1] == 'PRIVMSG' and cmd[3] == ':auth': + if cmd[1] == 'PRIVMSG' and str(cmd[3]).replace('.','') == ':auth': cmd_copy = cmd.copy() cmd_copy[5] = '**********' - self.Base.logs.debug(cmd_copy) + self.Base.logs.info(cmd_copy) else: self.Base.logs.info(cmd) else: diff --git a/install.py b/install.py new file mode 100644 index 0000000..a279768 --- /dev/null +++ b/install.py @@ -0,0 +1,263 @@ +from subprocess import check_call, run, CalledProcessError, PIPE +from platform import python_version +from sys import exit +import os, logging, shutil, pwd + + +class Install: + + def __init__(self) -> None: + + # Python required version + self.python_min_version = '3.10' + self.log_file = 'install.log' + self.ServiceName = 'Defender' + self.venv_name = '.pyenv' + self.venv_dependencies: list[str] = ['sqlalchemy','psutil','requests'] + self.install_folder = os.getcwd() + self.osname = os.name + self.cmd_linux_requirements: list[str] = ['apt', 'install', '-y', 'python3', 'python3-pip', 'python3-venv'] + self.venv_pip_full_path = os.path.join(self.venv_name, f'bin{os.sep}pip') + self.venv_python_full_path = os.path.join(self.venv_name, f'bin{os.sep}python') + self.systemd_folder = '/etc/systemd/system/' + + # Init log system + self.init_log_system() + + # Exclude Windows OS + if self.osname == 'nt': + print('/!\\ Windows OS is not supported by this automatic installation /!\\') + self.Logs.critical('/!\\ Windows OS is not supported by this automatic install /!\\') + exit(5) + + if not self.is_root(): + exit(5) + + # Get the current user + self.system_username: str = input(f'What is the user ro run defender with ? [{os.getlogin()}] : ') + if str(self.system_username).strip() == '': + self.system_username = os.getlogin() + + self.get_user_information(self.system_username) + + self.Logs.debug(f'The user selected is: {self.system_username}') + self.Logs.debug(f'Operating system: {self.osname}') + + # Install linux dependencies + self.install_linux_dependencies() + + # Check python version + self.checkPythonVersion() + + # Create systemd service file + self.create_service_file() + + # Check if Env Exist | install environment | Install python dependencies + self.check_venv() + + # Create and start service + if self.osname != 'nt': + self.run_subprocess(['systemctl','daemon-reload']) + self.run_subprocess(['systemctl','start', self.ServiceName]) + self.run_subprocess(['systemctl','status', self.ServiceName]) + + # Clean the Installation + self.clean_installation() + + return None + + def is_installed(self) -> bool: + + is_installed = False + + # Check logs folder + if os.path.exists('logs'): + is_installed = True + + # Check db folder + if os.path.exists('db'): + is_installed = True + + return is_installed + + def is_root(self) -> bool: + + if os.geteuid() != 0: + print('/!\\ user must run install.py as root /!\\') + self.Logs.critical('/!\\ user must run install.py as root /!\\') + return False + elif os.geteuid() == 0: + return True + + def get_user_information(self, system_user: str) -> None: + + try: + username: tuple = pwd.getpwnam(system_user) + self.system_uid = username.pw_uid + self.system_gid = username.pw_gid + return None + + except KeyError as ke: + self.Logs.critical(f"This user [{system_user}] doesn't exist: {ke}") + print(f"This user [{system_user}] doesn't exist: {ke}") + exit(5) + + def init_log_system(self) -> None: + + # Init logs object + self.Logs = logging + self.Logs.basicConfig(level=logging.DEBUG, + filename=self.log_file, + encoding='UTF-8', + format='%(asctime)s - %(levelname)s - %(filename)s - %(lineno)d - %(funcName)s - %(message)s') + + self.Logs.debug('#################### STARTING INSTALLATION ####################') + + return None + + def clean_installation(self) -> None: + + # Chown the Python Env to non user privilege + self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}', + f'{os.path.join(self.install_folder, self.venv_name)}' + ] + ) + + # Chown the installation log file + self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}', + f'{os.path.join(self.install_folder, self.log_file)}' + ] + ) + return None + + def run_subprocess(self, command:list) -> None: + + try: + run_command = check_call(command) + self.Logs.debug(f'{command} - {run_command}') + print(f'{command} - {run_command}') + + except CalledProcessError as e: + print(f"Command failed :{e.returncode}") + self.Logs.critical(f"Command failed :{e.returncode}") + exit(5) + + def checkPythonVersion(self) -> bool: + """Test si la version de python est autorisée ou non + + Returns: + bool: True si la version de python est autorisé sinon False + """ + python_required_version = self.python_min_version.split('.') + python_current_version = python_version().split('.') + + self.Logs.debug(f'The current python version is: {python_version()}') + + if int(python_current_version[0]) < int(python_required_version[0]): + print(f"## Your python version must be greather than or equal to {self.python_min_version} ##") + self.Logs.critical(f'Your python version must be greather than or equal to {self.python_min_version}') + return False + elif int(python_current_version[1]) < int(python_required_version[1]): + print(f"### Your python version must be greather than or equal to {self.python_min_version} ###") + self.Logs.critical(f'Your python version must be greather than or equal to {self.python_min_version}') + return False + + print(f"===> Version of python : {python_version()} ==> OK") + self.Logs.debug(f'Version of python : {python_version()} ==> OK') + + return True + + def check_packages(self, package_name) -> bool: + + try: + # Run a command in the virtual environment's Python to check if the package is installed + run([self.venv_python_full_path, '-c', f'import {package_name}'], check=True, stdout=PIPE, stderr=PIPE) + return True + except CalledProcessError: + return False + + def check_venv(self) -> bool: + + if os.path.exists(self.venv_name): + + # Installer les dependances + self.install_dependencies() + return True + else: + self.run_subprocess(['python3', '-m', 'venv', self.venv_name]) + self.Logs.debug(f'Python Virtual env installed {self.venv_name}') + print(f'Python Virtual env installed {self.venv_name}') + + self.install_dependencies() + return False + + def create_service_file(self) -> None: + + if self.systemd_folder is None: + # If Windows, do not install systemd + return None + + if os.path.exists(f'{self.systemd_folder}{os.sep}{self.ServiceName}.service'): + print(f'/!\\ Service already created in the system /!\\') + self.Logs.warning('/!\\ Service already created in the system /!\\') + print(f'The service file will be regenerated') + self.Logs.warning('The service file will be regenerated') + + + contain = f'''[Unit] +Description={self.ServiceName} IRC Service + +[Service] +User={self.system_username} +ExecStart={os.path.join(self.install_folder, self.venv_python_full_path)} {os.path.join(self.install_folder, 'main.py')} +WorkingDirectory={self.install_folder} +SyslogIdentifier={self.ServiceName} +Restart=on-failure + +[Install] +WantedBy=multi-user.target +''' + + with open(f'{self.ServiceName}.service.generated', 'w+') as servicefile: + servicefile.write(contain) + servicefile.close() + print('Service file generated with current configuration') + self.Logs.debug('Service file generated with current configuration') + + source = f'{self.install_folder}{os.sep}{self.ServiceName}.service.generated' + self.run_subprocess(['chown','-R', f'{self.system_username}:{self.system_username}', source]) + destination = f'{self.systemd_folder}' + shutil.copy(source, destination) + os.rename(f'{self.systemd_folder}{os.sep}{self.ServiceName}.service.generated', f'{self.systemd_folder}{os.sep}{self.ServiceName}.service') + print(f'Service file moved to systemd folder {self.systemd_folder}') + self.Logs.debug(f'Service file moved to systemd folder {self.systemd_folder}') + + def install_linux_dependencies(self) -> None: + + self.run_subprocess(self.cmd_linux_requirements) + + return None + + def install_dependencies(self) -> None: + + try: + self.run_subprocess([self.venv_pip_full_path, 'cache', 'purge']) + self.run_subprocess([self.venv_python_full_path, '-m', 'pip', 'install', '--upgrade', 'pip']) + + if self.check_packages('greenlet') is None: + self.run_subprocess( + [self.venv_pip_full_path, 'install', '--only-binary', ':all:', 'greenlet'] + ) + + for module in self.venv_dependencies: + if not self.check_packages(module): + ### Trying to install missing python packages ### + self.run_subprocess([self.venv_pip_full_path, 'install', module]) + else: + self.Logs.debug(f'{module} already installed') + print(f"==> {module} already installed") + + except CalledProcessError as cpe: + self.Logs.critical(f'{cpe}') + +Install() \ No newline at end of file diff --git a/version.json b/version.json index ea705b3..b11fda4 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "5.0.5" + "version": "5.0.6" } \ No newline at end of file