#!.venv/bin/python """ This module implements an FTP server with custom commands for managing virtual users and handling CSV file uploads. """ import logging import os from hashlib import sha256 from logging.handlers import RotatingFileHandler from pathlib import Path from pyftpdlib.handlers import FTPHandler from pyftpdlib.servers import FTPServer from utils.authorizers.database_authorizer import DatabaseAuthorizer from utils.config import loader_ftp_csv as setting from utils.connect import file_management, user_admin # Configure logging (moved inside main function) logger = logging.getLogger(__name__) # Legacy authorizer kept for reference (not used anymore) # The DatabaseAuthorizer is now used for real-time database synchronization class ASEHandler(FTPHandler): """Custom FTP handler that extends FTPHandler with custom commands and file handling.""" def __init__(self: object, conn: object, server: object, ioloop: object = None) -> None: """Initializes the handler, adds custom commands, and sets up command permissions. Args: conn (object): The connection object. server (object): The FTP server object. ioloop (object): The I/O loop object. """ super().__init__(conn, server, ioloop) self.proto_cmds = FTPHandler.proto_cmds.copy() # Add custom FTP commands for managing virtual users - command in lowercase self.proto_cmds.update( { "SITE ADDU": { "perm": "M", "auth": True, "arg": True, "help": "Syntax: SITE ADDU USERNAME PASSWORD (add virtual user).", } } ) self.proto_cmds.update( { "SITE DISU": { "perm": "M", "auth": True, "arg": True, "help": "Syntax: SITE DISU USERNAME (disable virtual user).", } } ) self.proto_cmds.update( { "SITE ENAU": { "perm": "M", "auth": True, "arg": True, "help": "Syntax: SITE ENAU USERNAME (enable virtual user).", } } ) self.proto_cmds.update( { "SITE LSTU": { "perm": "M", "auth": True, "arg": None, "help": "Syntax: SITE LSTU (list virtual users).", } } ) def on_file_received(self: object, file: str) -> None: return file_management.on_file_received(self, file) def on_incomplete_file_received(self: object, file: str) -> None: """Removes partially uploaded files. Args: file: The path to the incomplete file. """ os.remove(file) def ftp_SITE_ADDU(self: object, line: str) -> None: return user_admin.ftp_SITE_ADDU(self, line) def ftp_SITE_DISU(self: object, line: str) -> None: return user_admin.ftp_SITE_DISU(self, line) def ftp_SITE_ENAU(self: object, line: str) -> None: return user_admin.ftp_SITE_ENAU(self, line) def ftp_SITE_LSTU(self: object, line: str) -> None: return user_admin.ftp_SITE_LSTU(self, line) def setup_logging(log_filename: str): """ Configura il logging per il server FTP con rotation e output su console. Args: log_filename (str): Percorso del file di log. """ root_logger = logging.getLogger() formatter = logging.Formatter("%(asctime)s - PID: %(process)d.%(name)s.%(levelname)s: %(message)s") # Rimuovi eventuali handler esistenti if root_logger.hasHandlers(): root_logger.handlers.clear() # Handler per file con rotation (max 10MB per file, mantiene 5 backup) file_handler = RotatingFileHandler( log_filename, maxBytes=10 * 1024 * 1024, # 10 MB backupCount=5, # Mantiene 5 file di backup encoding="utf-8" ) file_handler.setFormatter(formatter) root_logger.addHandler(file_handler) # Handler per console (utile per Docker) console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) root_logger.addHandler(console_handler) root_logger.setLevel(logging.INFO) root_logger.info("Logging FTP configurato con rotation (10MB, 5 backup) e console output") def main(): """Main function to start the FTP server.""" # Load the configuration settings cfg = setting.Config() try: # Configure logging first setup_logging(cfg.logfilename) # Initialize the authorizer with database support # This authorizer checks the database on every login, ensuring # all FTP server instances stay synchronized without restarts authorizer = DatabaseAuthorizer(cfg) # Initialize handler handler = ASEHandler handler.cfg = cfg handler.authorizer = authorizer # Set masquerade address only if configured (importante per HA con VIP) # Questo è l'IP che il server FTP pubblicherà ai client per le connessioni passive if cfg.proxyaddr and cfg.proxyaddr.strip(): handler.masquerade_address = cfg.proxyaddr logger.info(f"FTP masquerade address configured: {cfg.proxyaddr}") else: logger.info("FTP masquerade address not configured - using server's default IP") # Set the range of passive ports for the FTP server _range = list(range(cfg.firstport, cfg.firstport + cfg.portrangewidth)) handler.passive_ports = _range # Log configuration logger.info(f"Starting FTP server on port {cfg.service_port} with DatabaseAuthorizer") logger.info(f"FTP passive ports range: {cfg.firstport}-{cfg.firstport + cfg.portrangewidth - 1}") logger.info(f"Database connection: {cfg.dbuser}@{cfg.dbhost}:{cfg.dbport}/{cfg.dbname}") # Create and start the FTP server server = FTPServer(("0.0.0.0", cfg.service_port), handler) server.serve_forever() except Exception as e: logger.error("Exit with error: %s.", e) if __name__ == "__main__": main()