import asyncio import logging import os from hashlib import sha256 from pathlib import Path from utils.database.connection import connetti_db_async logger = logging.getLogger(__name__) # Sync wrappers for FTP commands (required by pyftpdlib) def ftp_SITE_ADDU(self: object, line: str) -> None: """Sync wrapper for ftp_SITE_ADDU_async.""" asyncio.run(ftp_SITE_ADDU_async(self, line)) def ftp_SITE_DISU(self: object, line: str) -> None: """Sync wrapper for ftp_SITE_DISU_async.""" asyncio.run(ftp_SITE_DISU_async(self, line)) def ftp_SITE_ENAU(self: object, line: str) -> None: """Sync wrapper for ftp_SITE_ENAU_async.""" asyncio.run(ftp_SITE_ENAU_async(self, line)) def ftp_SITE_LSTU(self: object, line: str) -> None: """Sync wrapper for ftp_SITE_LSTU_async.""" asyncio.run(ftp_SITE_LSTU_async(self, line)) # Async implementations async def ftp_SITE_ADDU_async(self: object, line: str) -> None: """ Adds a virtual user, creates their directory, and saves their details to the database. Args: line (str): A string containing the username and password separated by a space. """ cfg = self.cfg try: parms = line.split() user = os.path.basename(parms[0]) # Extract the username password = parms[1] # Get the password hash_value = sha256(password.encode("UTF-8")).hexdigest() # Hash the password except IndexError: self.respond("501 SITE ADDU failed. Command needs 2 arguments") else: try: # Create the user's directory Path(cfg.virtpath + user).mkdir(parents=True, exist_ok=True) except Exception as e: self.respond(f"551 Error in create virtual user path: {e}") else: try: # Add the user to the authorizer self.authorizer.add_user(str(user), hash_value, cfg.virtpath + "/" + user, perm=cfg.defperm) # Save the user to the database using async connection try: conn = await connetti_db_async(cfg) except Exception as e: logger.error(f"Database connection error: {e}") self.respond("501 SITE ADDU failed: Database error") return try: async with conn.cursor() as cur: # Use parameterized query to prevent SQL injection await cur.execute( f"INSERT INTO {cfg.dbname}.{cfg.dbusertable} (ftpuser, hash, virtpath, perm) VALUES (%s, %s, %s, %s)", (user, hash_value, cfg.virtpath + user, cfg.defperm), ) # autocommit=True in connection logger.info(f"User {user} created.") self.respond("200 SITE ADDU successful.") except Exception as e: self.respond(f"501 SITE ADDU failed: {e}.") logger.error(f"Error creating user {user}: {e}") finally: conn.close() except Exception as e: self.respond(f"501 SITE ADDU failed: {e}.") logger.error(f"Error in ADDU: {e}") async def ftp_SITE_DISU_async(self: object, line: str) -> None: """ Removes a virtual user from the authorizer and marks them as deleted in the database. Args: line (str): A string containing the username to be disabled. """ cfg = self.cfg parms = line.split() user = os.path.basename(parms[0]) # Extract the username try: # Remove the user from the authorizer self.authorizer.remove_user(str(user)) # Delete the user from database try: conn = await connetti_db_async(cfg) except Exception as e: logger.error(f"Database connection error: {e}") self.respond("501 SITE DISU failed: Database error") return try: async with conn.cursor() as cur: # Use parameterized query to prevent SQL injection await cur.execute(f"UPDATE {cfg.dbname}.{cfg.dbusertable} SET disabled_at = NOW() WHERE ftpuser = %s", (user,)) # autocommit=True in connection logger.info(f"User {user} deleted.") self.respond("200 SITE DISU successful.") except Exception as e: logger.error(f"Error disabling user {user}: {e}") self.respond("501 SITE DISU failed.") finally: conn.close() except Exception as e: self.respond("501 SITE DISU failed.") logger.error(f"Error in DISU: {e}") async def ftp_SITE_ENAU_async(self: object, line: str) -> None: """ Restores a virtual user by updating their status in the database and adding them back to the authorizer. Args: line (str): A string containing the username to be enabled. """ cfg = self.cfg parms = line.split() user = os.path.basename(parms[0]) # Extract the username try: # Restore the user into database try: conn = await connetti_db_async(cfg) except Exception as e: logger.error(f"Database connection error: {e}") self.respond("501 SITE ENAU failed: Database error") return try: async with conn.cursor() as cur: # Enable the user await cur.execute(f"UPDATE {cfg.dbname}.{cfg.dbusertable} SET disabled_at = NULL WHERE ftpuser = %s", (user,)) # Fetch user details await cur.execute( f"SELECT ftpuser, hash, virtpath, perm FROM {cfg.dbname}.{cfg.dbusertable} WHERE ftpuser = %s", (user,) ) result = await cur.fetchone() if not result: self.respond(f"501 SITE ENAU failed: User {user} not found") return ftpuser, hash_value, virtpath, perm = result self.authorizer.add_user(ftpuser, hash_value, virtpath, perm) try: Path(cfg.virtpath + ftpuser).mkdir(parents=True, exist_ok=True) except Exception as e: self.respond(f"551 Error in create virtual user path: {e}") return logger.info(f"User {user} restored.") self.respond("200 SITE ENAU successful.") except Exception as e: logger.error(f"Error enabling user {user}: {e}") self.respond("501 SITE ENAU failed.") finally: conn.close() except Exception as e: self.respond("501 SITE ENAU failed.") logger.error(f"Error in ENAU: {e}") async def ftp_SITE_LSTU_async(self: object, line: str) -> None: """ Lists all virtual users from the database. Args: line (str): An empty string (no arguments needed for this command). """ cfg = self.cfg users_list = [] try: # Connect to the database to fetch users try: conn = await connetti_db_async(cfg) except Exception as e: logger.error(f"Database connection error: {e}") self.respond("501 SITE LSTU failed: Database error") return try: async with conn.cursor() as cur: self.push("214-The following virtual users are defined:\r\n") await cur.execute(f"SELECT ftpuser, perm, disabled_at FROM {cfg.dbname}.{cfg.dbusertable}") results = await cur.fetchall() for ftpuser, perm, disabled_at in results: users_list.append(f"Username: {ftpuser}\tPerms: {perm}\tDisabled: {disabled_at}\r\n") self.push("".join(users_list)) self.respond("214 LSTU SITE command successful.") except Exception as e: self.respond(f"501 list users failed: {e}") logger.error(f"Error listing users: {e}") finally: conn.close() except Exception as e: self.respond(f"501 list users failed: {e}") logger.error(f"Error in LSTU: {e}")