Skip to content

Commit

Permalink
- Factorisation dans une classe SftpAgricoll
Browse files Browse the repository at this point in the history
- Ajout logging (niveau INFO)
  • Loading branch information
alanzirek committed Mar 2, 2025
1 parent 62049f9 commit cb98f4b
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 214 deletions.
195 changes: 195 additions & 0 deletions core/agricoll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import base64
import datetime
import logging
import os
import subprocess
import tempfile

from django.conf import settings
import paramiko
from paramiko.client import SSHClient, MissingHostKeyPolicy
from paramiko.sftp_client import SFTPClient


class CleverCloudSftpVerifier(MissingHostKeyPolicy):
"""
Politique de vérification pour les connexions SFTP vers CleverCloud.
Vérifie à la fois le nom d'hôte et l'empreinte de la clé publique du serveur.
"""

# https://www.clever-cloud.com/developers/doc/addons/fs-bucket/#from-your-favorite-sftp-client
CLEVER_CLOUD_FINGERPRINTS = [
"SHA256:+ku6hhQb1O3OVzkZa2B+htPD+P+5K/X6QQYWXym/4Zo",
"SHA256:8tZzRvA3Fh9poG7g1bu8m0LQS819UBh7AYcEXJYiPqw",
"SHA256:HHGCP5cf0jQbQrIRXjiC9aYJGNQ+L9ijOmJUueLp+9A",
"SHA256:Hyt6ox+v2Lrvdfl29jwe1/dBq9zh2fmq2DO6rqurl7o",
"SHA256:drShQbl3Ox+sYYYP+urOCtuMiJFh7k1kECdvZ4hMuAE",
"SHA256:h1oUNRkYaIycchUsyAXPQHnu6MtTF2YUEYuisu+vnOE",
"SHA256:+550bmBCNAHscjOmKrdweueVUz2E6h1KzmSV+0c0U7w",
"SHA256:1O7d6cdmqj42Dw4nX90Y+6zIFTUI+aIwD0SLMQuj0ko",
"SHA256:AkHQnQXJ1lFEtliLHl8hlG7NiIZZgVn/uuRMCZJOKJk",
"SHA256:Atxhx7U0MOuZC7e4vs1tpyTJmNttB7d4+HNC5hiavFo",
"SHA256:Bla7GeL6hggg+rf6iDlKMrzIhxEBYB3VL7Q6PYGJYt4",
"SHA256:H5ZhQ/5JdMPSG49ojUNEhwSuRD663mnIJb/YDFFntyk",
"SHA256:TZr6eFrzoJmn4RS55Tb6yTd+WV9lTGtW0q+uLVbI7IE",
"SHA256:ZYFb1AsB+q++NRf7yW8E5rNOfxTRwjpJt6hqFP/NBNs",
"SHA256:d+nTyowvYtcxF28mCUu1ilqPJuLMExGyJ16Sv/pvoVY",
"SHA256:flpv4s3VxOrQFc/IG+BpR1s9dgDvR07A6zunNqO4Co0",
"SHA256:hvZN8rgSG82weLOeMTXdh1VwhjuRv+MJNnUt/X9R39g",
"SHA256:ls20B8C6Jdqx7RPQAjzVX7KmnrHizJum2sEvNhMcl60",
"SHA256:u1AzFc2AdFmlPRdNIZsn0sQJ/CKbfC2ZmXnQfabPek4",
"SHA256:wUPBX3X5gALgxXqD+IwG5qPRb0jbiOZ8/U1BOZeNhtk",
"SHA256:yRHC/tAlBpHLlRZ5rwbZ1z+159Bj3yg0VxHf+hXINLg",
"SHA256:yhn79aqxOGQZ+LXdN1/vIY+jwRIbBamlVT1+HdFoA6o",
]
EXPECTED_HOSTNAME = settings.SFTP_HOST

def missing_host_key(self, client, hostname, key):
if hostname != self.EXPECTED_HOSTNAME:
raise paramiko.SSHException("Connexion refusée - host non autorisé")
if key.fingerprint not in self.CLEVER_CLOUD_FINGERPRINTS:
raise paramiko.SSHException("Connexion refusée - empreinte de clé non reconnue")


class SftpAgricoll:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.client: SSHClient | None = None
self.sftp: SFTPClient | None = None
self.encrypted_data_file_path: str = os.path.join(settings.BASE_DIR, "contacts_agricoll.csv.encrypted")
self.encrypted_symmetric_key_file_path: str = os.path.join(settings.BASE_DIR, "symmetric.key.encrypted")
self.data_file_path = os.path.join(settings.BASE_DIR, "contacts_agricoll.csv")
self.symmetric_key_file_path: str = os.path.join(settings.BASE_DIR, "symmetric.key")

def connect_to_sftp(self):
self.logger.info("Connexion au serveur SFTP...")
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(CleverCloudSftpVerifier())
self.client.connect(
hostname=settings.SFTP_HOST,
username=settings.SFTP_USERNAME,
password=settings.SFTP_PASSWORD,
port=settings.SFTP_PORT,
)
self.sftp = self.client.open_sftp()
self.logger.info("Connexion SFTP établie")

def print_files(self):
print("Liste des fichiers disponibles:")
for file_attr in self.sftp.listdir_attr():
mod_time = datetime.datetime.fromtimestamp(file_attr.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
print(f"- {file_attr.filename}, Modifié: {mod_time})")

def get_latest_encrypted_filenames(self) -> tuple[str, str]:
"""Trouve les fichiers chiffrés (fichier de données et la clé symétrique) les plus récents sur le serveur SFTP"""
file_list = self.sftp.listdir_attr()

if not file_list:
raise FileNotFoundError("Aucun fichier trouvé sur le serveur SFTP")

latest_encrypted_data_file_attrs = None
latest_encrypted_symmetric_key_file_attrs = None
encrypted_data_file_suffix = ".encrypted"
encrypted_symmetric_key_file_suffix = ".key.encrypted"
for file_attr in file_list:
if file_attr.filename.endswith(encrypted_data_file_suffix) and not file_attr.filename.endswith(
encrypted_symmetric_key_file_suffix
):
if (
latest_encrypted_data_file_attrs is None
or file_attr.st_mtime > latest_encrypted_data_file_attrs.st_mtime
):
latest_encrypted_data_file_attrs = file_attr
elif file_attr.filename.endswith(encrypted_symmetric_key_file_suffix):
if (
latest_encrypted_symmetric_key_file_attrs is None
or file_attr.st_mtime > latest_encrypted_symmetric_key_file_attrs.st_mtime
):
latest_encrypted_symmetric_key_file_attrs = file_attr

if not latest_encrypted_data_file_attrs:
raise FileNotFoundError("Aucun fichier de données chiffré (.encrypted) trouvé sur le serveur SFTP")
if not latest_encrypted_symmetric_key_file_attrs:
raise FileNotFoundError("Aucune clé symétrique chiffrée (.key.encrypted) trouvé sur le serveur SFTP")

return latest_encrypted_data_file_attrs.filename, latest_encrypted_symmetric_key_file_attrs.filename

def download_files(
self,
remote_encrypted_data_filename: str,
remote_encrypted_symmetric_key_filename: str,
):
self.logger.info(
f"Téléchargement de {remote_encrypted_data_filename} et {remote_encrypted_symmetric_key_filename}..."
)
self.sftp.get(remote_encrypted_data_filename, self.encrypted_data_file_path)
self.sftp.get(remote_encrypted_symmetric_key_filename, self.encrypted_symmetric_key_file_path)
self.logger.info("Fichiers téléchargés")

def _get_private_key_content(self) -> bytes:
private_key_base64 = os.environ.get("SFTP_PRIVATE_KEY")
if not private_key_base64:
raise KeyError("Variable d'environnement SFTP_PRIVATE_KEY requise")
return base64.b64decode(private_key_base64)

def decrypt_symmetric_key(self):
self.logger.info("Déchiffrement de la clé symétrique...")
with tempfile.NamedTemporaryFile() as temp_private_key_file:
temp_private_key_file.write(self._get_private_key_content())
temp_private_key_file.flush()
try:
subprocess.run(
[
"openssl",
"rsautl",
"-decrypt",
"-inkey",
temp_private_key_file.name,
"-in",
self.encrypted_symmetric_key_file_path,
"-out",
self.symmetric_key_file_path,
],
check=True,
)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Erreur lors du déchiffrement de la clé symétrique: {str(e)}")
self.logger.info("Clé symétrique déchiffrée")

def decrypt_data_file(self):
self.logger.info("Déchiffrement du fichier de données...")
try:
subprocess.run(
[
"openssl",
"enc",
"-d",
"-aes-256-cbc",
"-a",
"-salt",
"-pbkdf2",
"-in",
self.encrypted_data_file_path,
"-out",
self.data_file_path,
"-pass",
f"file:{self.symmetric_key_file_path}",
],
check=True,
)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Erreur lors du déchiffrement du fichier de données: {str(e)}")
self.logger.info("Fichier de données déchiffré")
return self.data_file_path

def clean_files(self):
self.logger.info("Suppression des fichiers locaux...")
os.remove(self.encrypted_symmetric_key_file_path)
os.remove(self.symmetric_key_file_path)
os.remove(self.encrypted_data_file_path)
os.remove(self.data_file_path)
self.logger.info("Fichiers locaux supprimés")

def close_connections(self):
self.sftp.close()
self.client.close()
Loading

0 comments on commit cb98f4b

Please sign in to comment.