Compare commits
2 Commits
8f5c0a5016
...
b5d3e764e8
| Author | SHA1 | Date | |
|---|---|---|---|
| b5d3e764e8 | |||
| d865b7daf2 |
70
change_vm_ssh_port.sh
Executable file
70
change_vm_ssh_port.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Script per cambiare la porta SSH delle VM da 22 a 2222
|
||||
# Questo permette ai container SFTP di usare la porta 22 standard
|
||||
|
||||
set -e
|
||||
|
||||
NEW_SSH_PORT=2222
|
||||
SSH_CONFIG="/etc/ssh/sshd_config"
|
||||
|
||||
echo "================================================"
|
||||
echo "Cambio porta SSH della VM da 22 a $NEW_SSH_PORT"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Backup della configurazione originale
|
||||
if [ ! -f "${SSH_CONFIG}.backup" ]; then
|
||||
echo "Creazione backup di $SSH_CONFIG..."
|
||||
cp "$SSH_CONFIG" "${SSH_CONFIG}.backup"
|
||||
echo "✓ Backup creato: ${SSH_CONFIG}.backup"
|
||||
else
|
||||
echo "✓ Backup già esistente: ${SSH_CONFIG}.backup"
|
||||
fi
|
||||
|
||||
# Modifica la porta SSH
|
||||
echo ""
|
||||
echo "Modifica della porta SSH a $NEW_SSH_PORT..."
|
||||
|
||||
# Rimuovi eventuali configurazioni Port esistenti e aggiungi la nuova
|
||||
grep -v "^Port " "$SSH_CONFIG" | grep -v "^#Port " > "${SSH_CONFIG}.tmp"
|
||||
echo "Port $NEW_SSH_PORT" | cat - "${SSH_CONFIG}.tmp" > "$SSH_CONFIG"
|
||||
rm "${SSH_CONFIG}.tmp"
|
||||
|
||||
echo "✓ Configurazione SSH aggiornata"
|
||||
|
||||
# Verifica la configurazione
|
||||
echo ""
|
||||
echo "Verifica della configurazione SSH..."
|
||||
if sshd -t; then
|
||||
echo "✓ Configurazione SSH valida"
|
||||
else
|
||||
echo "✗ ERRORE: Configurazione SSH non valida!"
|
||||
echo "Ripristino del backup..."
|
||||
cp "${SSH_CONFIG}.backup" "$SSH_CONFIG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Riavvia il servizio SSH
|
||||
echo ""
|
||||
echo "Riavvio del servizio SSH..."
|
||||
systemctl restart sshd
|
||||
echo "✓ Servizio SSH riavviato"
|
||||
|
||||
# Mostra lo status
|
||||
echo ""
|
||||
echo "Status del servizio SSH:"
|
||||
systemctl status sshd --no-pager -l | head -10
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "✓ Porta SSH cambiata con successo!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "IMPORTANTE:"
|
||||
echo " - SSH ora ascolta sulla porta $NEW_SSH_PORT"
|
||||
echo " - Connettiti con: ssh -p $NEW_SSH_PORT root@<ip-vm>"
|
||||
echo " - La porta 22 è ora disponibile per i container SFTP"
|
||||
echo ""
|
||||
echo "Per verificare:"
|
||||
echo " ss -tlnp | grep sshd"
|
||||
echo ""
|
||||
@@ -84,6 +84,7 @@ services:
|
||||
environment:
|
||||
DB_HOST: ${VIP:-192.168.1.210}
|
||||
FTP_INSTANCE_ID: 1
|
||||
FTP_MODE: ftp
|
||||
TZ: Europe/Rome
|
||||
FTP_PASSIVE_PORT: "40000"
|
||||
FTP_EXTERNAL_IP: ${VIP:-192.168.1.210}
|
||||
@@ -100,6 +101,27 @@ services:
|
||||
- "21"
|
||||
labels:
|
||||
logging: "promtail"
|
||||
sftp-server-1:
|
||||
build: .
|
||||
container_name: sftp-server-1
|
||||
restart: unless-stopped
|
||||
command: ["python", "-m", "src.ftp_csv_receiver"]
|
||||
environment:
|
||||
DB_HOST: ${VIP:-192.168.1.210}
|
||||
FTP_INSTANCE_ID: 11
|
||||
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
|
||||
@@ -111,6 +133,7 @@ services:
|
||||
- app-network
|
||||
ports:
|
||||
- "21:21"
|
||||
- "22:22"
|
||||
- "8404:8404"
|
||||
labels:
|
||||
logging: "promtail"
|
||||
|
||||
12
vm1/generate_ssh_host_key.sh
Executable file
12
vm1/generate_ssh_host_key.sh
Executable 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
|
||||
@@ -31,3 +31,11 @@ frontend ftp_control
|
||||
backend ftp_servers
|
||||
mode tcp
|
||||
server ftp1 ftp-server-1:21 check
|
||||
|
||||
frontend sftp_control
|
||||
bind *:22
|
||||
default_backend sftp_servers
|
||||
|
||||
backend sftp_servers
|
||||
mode tcp
|
||||
server sftp1 sftp-server-1:22 check
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
194
vm1/src/utils/servers/sftp_server.py
Normal file
194
vm1/src/utils/servers/sftp_server.py
Normal 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
|
||||
@@ -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
12
vm2/generate_ssh_host_key.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__":
|
||||
|
||||
194
vm2/src/utils/servers/sftp_server.py
Normal file
194
vm2/src/utils/servers/sftp_server.py
Normal 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
|
||||
Reference in New Issue
Block a user