Files
proxmox-ha-setup/vm2/src/ftp_csv_receiver.py
2025-11-02 16:33:16 +01:00

246 lines
8.3 KiB
Python
Executable File

#!.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 <SP> ADDU USERNAME PASSWORD (add virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE DISU": {
"perm": "M",
"auth": True,
"arg": True,
"help": "Syntax: SITE <SP> DISU USERNAME (disable virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE ENAU": {
"perm": "M",
"auth": True,
"arg": True,
"help": "Syntax: SITE <SP> ENAU USERNAME (enable virtual user).",
}
}
)
self.proto_cmds.update(
{
"SITE LSTU": {
"perm": "M",
"auth": True,
"arg": None,
"help": "Syntax: SITE <SP> 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()