aggiunti server sftp

This commit is contained in:
2025-11-02 16:33:16 +01:00
parent d865b7daf2
commit b5d3e764e8
15 changed files with 674 additions and 22 deletions

View File

@@ -58,6 +58,7 @@ services:
environment:
DB_HOST: ${VIP:-192.168.1.210}
FTP_INSTANCE_ID: 2
FTP_MODE: ftp
TZ: Europe/Rome
FTP_PASSIVE_PORT: "40000"
FTP_EXTERNAL_IP: ${VIP:-192.168.1.210}
@@ -74,6 +75,27 @@ services:
- "21"
labels:
logging: "promtail"
sftp-server-2:
build: .
container_name: sftp-server-2
restart: unless-stopped
command: ["python", "-m", "src.ftp_csv_receiver"]
environment:
DB_HOST: ${VIP:-192.168.1.210}
FTP_INSTANCE_ID: 12
FTP_MODE: sftp
TZ: Europe/Rome
volumes:
- app-logs:/app/logs
- ./aseftp:/app/aseftp
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
networks:
- app-network
expose:
- "22"
labels:
logging: "promtail"
haproxy:
image: haproxy:2.8-alpine
@@ -85,6 +107,7 @@ services:
- app-network
ports:
- "21:21"
- "22:22"
- "8404:8404"
labels:
logging: "promtail"

12
vm2/generate_ssh_host_key.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Generate SSH host key for SFTP server if it doesn't exist
KEY_FILE="/app/ssh_host_key"
if [ ! -f "$KEY_FILE" ]; then
echo "Generating SSH host key for SFTP server..."
ssh-keygen -t rsa -b 4096 -f "$KEY_FILE" -N "" -C "SFTP-Server-Host-Key"
echo "SSH host key generated successfully at $KEY_FILE"
else
echo "SSH host key already exists at $KEY_FILE"
fi

View File

@@ -31,3 +31,11 @@ frontend ftp_control
backend ftp_servers
mode tcp
server ftp2 ftp-server-2:21 check
frontend sftp_control
bind *:22
default_backend sftp_servers
backend sftp_servers
mode tcp
server sftp2 sftp-server-2:22 check

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"aiomysql>=0.2.0",
"asyncssh>=2.14.0", # Required for SFTP server mode
"cryptography>=45.0.3",
"mysql-connector-python>=9.3.0", # Needed for synchronous DB connections (ftp_csv_receiver.py, load_ftp_users.py)
"pyftpdlib>=2.0.1",

View File

@@ -1,11 +1,17 @@
#!.venv/bin/python
"""
This module implements an FTP server with custom commands for
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
@@ -140,15 +146,9 @@ def setup_logging(log_filename: str):
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()
def start_ftp_server(cfg):
"""Start traditional FTP server."""
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
@@ -185,7 +185,60 @@ def main():
server.serve_forever()
except Exception as e:
logger.error("Exit with error: %s.", 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__":

View File

@@ -17,7 +17,7 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(
logger = logging.getLogger(__name__)
# Configurazione server FTP
FTP_CONFIG = {"host": "192.168.1.210", "user": "admin", "password": "batt1l0", "port": 21}
FTP_CONFIG = {"host": "localhost", "user": "admin", "password": "batt1l0", "port": 2121}
def connect_ftp() -> FTP:

View File

@@ -0,0 +1,194 @@
"""
SFTP Server implementation using asyncssh.
Shares the same authentication system and file handling logic as the FTP server.
"""
import asyncio
import logging
from pathlib import Path
import asyncssh
from utils.connect import file_management
from utils.database.connection import connetti_db_async
logger = logging.getLogger(__name__)
class ASESFTPServer(asyncssh.SFTPServer):
"""Custom SFTP server that handles file uploads with the same logic as FTP server."""
def __init__(self, chan):
"""Initialize SFTP server with channel."""
super().__init__(chan)
# Get config from connection (set during authentication)
self.cfg = chan.get_connection()._cfg
async def close(self):
"""Called when SFTP session is closed."""
logger.info(f"SFTP session closed for user: {self._chan.get_connection().get_extra_info('username')}")
await super().close()
class ASESSHServer(asyncssh.SSHServer):
"""Custom SSH server for SFTP authentication using database."""
def __init__(self, cfg):
"""Initialize SSH server with configuration."""
self.cfg = cfg
super().__init__()
def connection_made(self, conn):
"""Called when connection is established."""
# Store config in connection for later use
conn._cfg = self.cfg
logger.info(f"SSH connection from {conn.get_extra_info('peername')[0]}")
def connection_lost(self, exc):
"""Called when connection is lost."""
if exc:
logger.error(f"SSH connection lost: {exc}")
async def validate_password(self, username, password):
"""
Validate user credentials against database.
Same logic as DatabaseAuthorizer for FTP.
"""
from hashlib import sha256
# Hash the provided password
password_hash = sha256(password.encode("UTF-8")).hexdigest()
# Check if user is admin
if username == self.cfg.adminuser[0]:
if self.cfg.adminuser[1] == password_hash:
logger.info(f"Admin user '{username}' authenticated successfully")
return True
else:
logger.warning(f"Failed admin login attempt for user: {username}")
return False
# For regular users, check database
try:
conn = await connetti_db_async(self.cfg)
cur = await conn.cursor()
# Query user from database
await cur.execute(
f"SELECT ftpuser, hash, virtpath, perm, disabled_at FROM {self.cfg.dbname}.{self.cfg.dbusertable} WHERE ftpuser = %s",
(username,)
)
result = await cur.fetchone()
await cur.close()
conn.close()
if not result:
logger.warning(f"SFTP login attempt for non-existent user: {username}")
return False
ftpuser, stored_hash, virtpath, perm, disabled_at = result
# Check if user is disabled
if disabled_at is not None:
logger.warning(f"SFTP login attempt for disabled user: {username}")
return False
# Verify password
if stored_hash != password_hash:
logger.warning(f"Invalid password for SFTP user: {username}")
return False
# Authentication successful - ensure user directory exists
try:
Path(virtpath).mkdir(parents=True, exist_ok=True)
except Exception as e:
logger.error(f"Failed to create directory for user {username}: {e}")
return False
logger.info(f"Successful SFTP login for user: {username}")
return True
except Exception as e:
logger.error(f"Database error during SFTP authentication for user {username}: {e}", exc_info=True)
return False
def password_auth_supported(self):
"""Enable password authentication."""
return True
def begin_auth(self, username):
"""Called when authentication begins."""
logger.debug(f"Authentication attempt for user: {username}")
return True
class SFTPFileHandler(asyncssh.SFTPServer):
"""Extended SFTP server with file upload handling."""
def __init__(self, chan):
super().__init__(chan)
self.cfg = chan.get_connection()._cfg
async def close(self):
"""Handle session close."""
await super().close()
# Override file operations to add custom handling
async def rename(self, oldpath, newpath):
"""
Handle file rename/move - called when upload completes.
This is where we trigger the CSV processing like in FTP.
"""
result = await super().rename(oldpath, newpath)
# Check if it's a CSV file that was uploaded
if newpath.lower().endswith('.csv'):
try:
# Trigger file processing (same as FTP on_file_received)
logger.info(f"CSV file uploaded via SFTP: {newpath}")
# Create a mock handler object with required attributes
mock_handler = type('obj', (object,), {
'cfg': self.cfg,
'username': self._chan.get_connection().get_extra_info('username')
})()
# Call the same file_management function used by FTP
file_management.on_file_received(mock_handler, newpath)
except Exception as e:
logger.error(f"Error processing SFTP uploaded file {newpath}: {e}", exc_info=True)
return result
async def start_sftp_server(cfg, host='0.0.0.0', port=22):
"""
Start SFTP server.
Args:
cfg: Configuration object
host: Host to bind to
port: Port to bind to
Returns:
asyncssh server object
"""
logger.info(f"Starting SFTP server on {host}:{port}")
# Create SSH server
ssh_server = ASESSHServer(cfg)
# Start asyncssh server
server = await asyncssh.create_server(
lambda: ssh_server,
host,
port,
server_host_keys=['/app/ssh_host_key'], # You'll need to generate this
sftp_factory=SFTPFileHandler,
session_encoding=None, # Binary mode for file transfers
)
logger.info(f"SFTP server started successfully on {host}:{port}")
logger.info(f"Database connection: {cfg.dbuser}@{cfg.dbhost}:{cfg.dbport}/{cfg.dbname}")
return server