#!.venv/bin/python """ This module implements an FTP/SFTP server with custom commands for managing virtual users and handling CSV file uploads. Server mode is controlled by FTP_MODE environment variable: - FTP_MODE=ftp (default): Traditional FTP server - FTP_MODE=sftp: SFTP (SSH File Transfer Protocol) server """ import asyncio import logging import os import sys 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.""" # Permetti connessioni dati da indirizzi IP diversi (importante per NAT/proxy) permit_foreign_addresses = True 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 start_ftp_server(cfg): """Start traditional FTP server.""" try: # 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 passive_ports_range = list(range(cfg.firstport, cfg.firstport + cfg.portrangewidth)) handler.passive_ports = passive_ports_range # Log configuration logger.info(f"Starting FTP server on port {cfg.service_port} with DatabaseAuthorizer") logger.info( f"FTP passive ports configured: {cfg.firstport}-{cfg.firstport + cfg.portrangewidth - 1} " f"({len(passive_ports_range)} ports)" ) logger.info(f"FTP permit_foreign_addresses: {handler.permit_foreign_addresses}") 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("FTP server error: %s", e, exc_info=True) sys.exit(1) async def start_sftp_server_async(cfg): """Start SFTP server (async).""" try: from utils.servers.sftp_server import start_sftp_server logger.info(f"Starting SFTP server on port {cfg.service_port}") logger.info(f"Database connection: {cfg.dbuser}@{cfg.dbhost}:{cfg.dbport}/{cfg.dbname}") # Start SFTP server server = await start_sftp_server(cfg, host="0.0.0.0", port=cfg.service_port) # Keep server running await asyncio.Event().wait() except ImportError as e: logger.error("SFTP mode requires 'asyncssh' library. Install with: pip install asyncssh") logger.error(f"Error: {e}") sys.exit(1) except Exception as e: logger.error("SFTP server error: %s", e, exc_info=True) sys.exit(1) def main(): """Main function to start FTP or SFTP server based on FTP_MODE environment variable.""" # Load the configuration settings cfg = setting.Config() # Configure logging first setup_logging(cfg.logfilename) # Get server mode from environment variable (default: ftp) server_mode = os.getenv("FTP_MODE", "ftp").lower() if server_mode not in ["ftp", "sftp"]: logger.error(f"Invalid FTP_MODE: {server_mode}. Valid values: ftp, sftp") sys.exit(1) logger.info(f"Server mode: {server_mode.upper()}") try: if server_mode == "ftp": start_ftp_server(cfg) elif server_mode == "sftp": asyncio.run(start_sftp_server_async(cfg)) except KeyboardInterrupt: logger.info("Server stopped by user") except Exception as e: logger.error("Unexpected error: %s", e, exc_info=True) sys.exit(1) if __name__ == "__main__": main()