Cet article présente une implémentation complète d'un serveur FTP capable de gérer plusieurs utilisateurs simultanément. Le système offre une sécurité renforcée avec une authentification cryptée, des espaces personnels isolés pour chaque utilisateur, et des fonctionnalités avancées de transfert de fichiers.
Exigences Fonctionnelles
- Authentification sécurisée des utilisateurs avec cryptage
- Connexion simultanée de plusieurs utilisateurs
- Espace personnel dédié pour chaque utilisateur avec isolation des espaces
- Quotas de stockage personnalisables par utilisateur
- Naviguation libre dans les répertoires du serveur
- Visualisation des fichiers dans le répertoire courant
- Transferts de fichiers montants et descendants avec intégrité garantie
- Affichage d'une barre de progression pendant les transferts
- Fonction supplémentaire: reprise des transferts interrompus
Fonctionnalités Implémentées
- Authentification sécurisée des utilisatuers
- Connexion simultanée de plusieurs utilisateurs
- Espace personnel isolé pour chaque utilisateur
- Transferts de fichiers avec intégrité vérifiée
- Affichage d'une barre de progression pendant les transferts
Architecture du Programme
Composant Serveur FTP
ServeurFTP
├── bin
│ └── demarrer_serveur.py
├── conf
│ ├── comptes.cfg
│ └── configuration.py
├── core
│ ├── gestion_ftp.py
│ └── principal.py
├── home
│ ├── utilisateur001
│ └── utilisateur002
└── logs
Composant Client FTP
└── client_ftp.py
Code du Serveur
Script de Démarrage (bin/demarrer_serveur.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
CHEIN_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(CHEIN_BASE)
from core import principal
if __name__ == '__main__':
principal.GestionArguments()
Fichier de Configuration des Comptes (conf/comptes.cfg)
[PAR_DEFAUT]
[utilisateur001]
MotDePasse = 123
Quota = 100
[utilisateur002]
MotDePasse = 123
Quota = 100
Paramètres Système (conf/configuration.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
CHEIN_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPERTOIRE_UTILISATEURS = "%s/home" % CHEIN_BASE
REPERTOIRE_JOURNAL = "%s/log" % CHEIN_BASE
NIVEAU_JOURNAL = "DEBUG"
FICHIER_COMPTE = "%s/conf/comptes.cfg" % CHEIN_BASE
ADRESSE_HOTE = "127.0.0.1"
PORT = 9999
Logique du Serveur FTP (core/gestion_ftp.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socketserver
import json
import configparser
import os
import hashlib
from conf import configuration
CODES_STATUT = {
250:"Format de commande invalide, ex: {'action':'recevoir','nomfichier':'test.py','taille':344}",
251:"Commande invalide",
252:"Données d'authentification invalides",
253:"Nom d'utilisateur ou mot de passe incorrect",
254:"Authentification réussie",
255:"Nom de fichier non fourni",
256:"Le fichier n'existe pas sur le serveur",
257:"Prêt à envoyer le fichier",
258:"Vérification md5",
}
class GestionnaireFTP(socketserver.BaseRequestHandler):
def handle(self):
'''Traite les messages entrants du client'''
while True:
self.donnees = self.request.recv(1024).strip()
print(self.client_address[0])
print(self.donnees)
if not self.donnees:
print("client déconnecté...")
break
paquet = json.loads(self.donnees.decode())
if paquet.get('action') is not None:
print("---->", hasattr(self, "_%s" % paquet.get('action')))
if hasattr(self, "_%s" % paquet.get('action')):
methode = getattr(self, "_%s" % paquet.get('action'))
methode(paquet)
else:
print("commande invalide")
self.envoyer_reponse(251)
else:
print("format de commande invalide")
self.envoyer_reponse(250)
def envoyer_reponse(self,code_statut,donnees=None):
'''Envoie une réponse au client'''
reponse = {'code_statut':code_statut,'message_statut':CODES_STATUT[code_statut]}
if donnees:
reponse.update(donnees)
self.request.send(json.dumps(reponse).encode())
def _authentifier(self,*args,**kwargs):
'''Valide les informations d'authentification du client'''
donnees = args[0]
if donnees.get("nomutilisateur") is None or donnees.get("motdepasse") is None:
self.envoyer_reponse(252)
utilisateur = self.valider_identite(donnees.get("nomutilisateur"),donnees.get("motdepasse"))
if utilisateur is None:
self.envoyer_reponse(253)
else:
print("authentification réussie",utilisateur)
self.utilisateur = utilisateur
self.envoyer_reponse(254)
def valider_identite(self,nomutilisateur,motdepasse):
'''Vérifie la validité des credentials utilisateur'''
config = configparser.ConfigParser()
config.read(configuration.FICHIER_COMPTE)
if nomutilisateur in config.sections():
_motdepasse = config[nomutilisateur]["MotDePasse"]
if _motdepasse == motdepasse:
print("authentification réussie pour",nomutilisateur)
config[nomutilisateur]["NomUtilisateur"] = nomutilisateur
return config[nomutilisateur]
def _envoyer(self,*args,**kwargs):
"Le client envoie un fichier au serveur"
donnees = args[0]
nom_fichier_base = donnees.get('nomfichier')
fichier_obj = open(nom_fichier_base, 'wb')
donnees = self.request.recv(4096)
fichier_obj.write(donnees)
fichier_obj.close()
def _recevoir(self,*args,**kwargs):
'''Méthode de téléchargement de fichiers'''
donnees = args[0]
if donnees.get('nomfichier') is None:
self.envoyer_reponse(255)
repertoire_home = "%s/%s" %(configuration.REPERTOIRE_UTILISATEURS,self.utilisateur["NomUtilisateur"])
chemin_complet_fichier = "%s/%s" %(repertoire_home,donnees.get('nomfichier'))
print("chemin complet du fichier",chemin_complet_fichier)
if os.path.isfile(chemin_complet_fichier):
fichier_obj = open(chemin_complet_fichier,'rb')
taille_fichier = os.path.getsize(chemin_complet_fichier)
self.envoyer_reponse(257,{'taille_fichier':taille_fichier})
self.request.recv(1)
if donnees.get('md5'):
objet_md5 = hashlib.md5()
for ligne in fichier_obj:
self.request.send(ligne)
objet_md5.update(ligne)
else:
fichier_obj.close()
valeur_md5 = objet_md5.hexdigest()
self.envoyer_reponse(258,{'md5':valeur_md5})
print("transmission du fichier terminée....")
else:
for ligne in fichier_obj:
self.request.send(ligne)
else:
fichier_obj.close()
print("transmission du fichier terminée....")
else:
self.envoyer_reponse(256)
def _lister(self,*args,**kwargs):
pass
def _changer_repertoire(self,*args,**kwargs):
pass
if __name__ == '__main__':
ADRESSE_HOTE, PORT = "127.0.0.1", 9999
Module Principal (core/principal.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import optparse
from core.gestion_ftp import GestionnaireFTP
import socketserver
from conf import configuration
class GestionArguments(object):
def __init__(self):
self.analyseur = optparse.OptionParser()
(options, args) = self.analyseur.parse_args()
self.valider_arguments(options, args)
def valider_arguments(self,options,args):
'''Valide et appelle la fonctionnalité correspondante'''
if hasattr(self,args[0]):
fonction = getattr(self,args[0])
fonction()
else:
self.analyseur.print_help()
def demarrer(self):
print('---démarrage du serveur---')
serveur = socketserver.ThreadingTCPServer((configuration.ADRESSE_HOTE, configuration.PORT), GestionnaireFTP)
serveur.serve_forever()
Client FTP (client_ftp.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import socket
import os
import sys
import optparse
import json
import hashlib
CODES_STATUT = {
250:"Format de commande invalide, ex: {'action':'recevoir','nomfichier':'test.py','taille':344}",
251:"Commande invalide",
252:"Données d'authentification invalides",
253:"Nom d'utilisateur ou mot de passe incorrect",
254:"Authentification réussie",
255:"Nom de fichier non fourni",
256:"Le fichier n'existe pas sur le serveur",
257:"Prêt à envoyer le fichier",
}
class ClientFTP(object):
def __init__(self):
analyseur = optparse.OptionParser()
analyseur.add_option("-s","--serveur",dest="serveur",help="adresse IP du serveur FTP")
analyseur.add_option("-P","--port",type="int",dest="port",help="port du serveur FTP")
analyseur.add_option("-u","--utilisateur",dest="utilisateur",help="nom d'utilisateur")
analyseur.add_option("-p","--motdepasse",dest="motdepasse",help="mot de passe")
self.options,self.arguments = analyseur.parse_args()
self.valider_arguments(self.options,self.arguments)
self.etablir_connexion()
def etablir_connexion(self):
'''Établit une connexion distante'''
self.socket = socket.socket()
self.socket.connect((self.options.serveur,self.options.port))
def valider_arguments(self,options,arguments):
'''Valide la validité des paramètres'''
if options.utilisateur is not None and options.motdepasse is not None:
pass
elif options.utilisateur is None and options.motdepasse is None:
pass
else:
exit("Erreur: nom d'utilisateur et mot de passe doivent être fournis ensemble...")
if options.serveur and options.port:
if options.port >0 and options.port <65535:
return True
else:
exit("Erreur: le port doit être compris entre 0 et 65535")
def authentifier(self):
'''Authentifie l'utilisateur et récupère les informations saisies par le client'''
if self.options.utilisateur:
print(self.options.utilisateur,self.options.motdepasse)
return self.obtenir_resultat_auth(self.options.utilisateur,self.options.motdepasse)
else:
tentative_restante = 0
while tentative_restante <3:
nom_utilisateur = input("nom d'utilisateur: ").strip()
mot_de_passe = input("mot de passe: ").strip()
return self.obtenir_resultat_auth(nom_utilisateur,mot_de_passe)
def obtenir_resultat_auth(self,utilisateur,motdepasse):
'''Le serveur distant évalue les informations utilisateur, mot de passe et action'''
donnees = {'action':'authentifier',
'utilisateur':utilisateur,
'motdepasse':motdepasse,}
self.socket.send(json.dumps(donnees).encode())
reponse = self.obtenir_reponse()
if reponse.get('code_statut') == 254:
print("Authentification réussie!")
self.utilisateur = utilisateur
return True
else:
print(reponse.get("message_statut"))
def obtenir_reponse(self):
'''Récupère le résultat de la réponse du serveur, méthode commune'''
donnees = self.socket.recv(1024)
donnees = json.loads(donnees.decode())
return donnees
def interactif(self):
'''Programme interactif'''
if self.authentifier():
print("--début de l'interaction...")
while True:
choix = input("[%s]:"%self.utilisateur).strip()
if len(choix) == 0:continue
liste_commandes = choix.split()
if hasattr(self,"_%s"%liste_commandes[0]):
methode = getattr(self,"_%s"%liste_commandes[0])
methode(liste_commandes)
else:
print("Commande invalide.")
def verification_md5_requise(self,liste_commandes):
'''Vérifie si la commande nécessite une vérification MD5'''
if '--md5' in liste_commandes:
return True
def afficher_progression(self,total):
'''Barre de progression'''
taille_recue = 0
pourcentage_actuel = 0
while taille_recue < total:
if int((taille_recue / total) * 100) > pourcentage_actuel :
print("#",end="",flush=True)
pourcentage_actuel = (taille_recue / total) * 100
nouvelle_taille = yield
taille_recue += nouvelle_taille
def _recevoir(self,liste_commandes):
'''Méthode de téléchargement de fichiers'''
print("recevoir--",liste_commandes)
if len(liste_commandes) == 1:
print("aucun nom de fichier spécifié...")
return
en_tete_client = {
'action':'recevoir',
'nomfichier':liste_commandes[1],
}
if self.verification_md5_requise(liste_commandes):
en_tete_client['md5'] = True
self.socket.send(json.dumps(en_tete_client).encode())
reponse = self.obtenir_reponse()
print(reponse)
if reponse["code_statut"] ==257:
self.socket.send(b'1')
nom_fichier_base = liste_commandes[1].split('/')[-1]
taille_recue = 0
fichier_obj = open(nom_fichier_base,'wb')
if self.verification_md5_requise(liste_commandes):
objet_md5 = hashlib.md5()
progression = self.afficher_progression(reponse['taille_fichier'])
progression.__next__()
while taille_recue < reponse['taille_fichier']:
donnees = self.socket.recv(4096)
taille_recue += len(donnees)
try:
progression.send(len(donnees))
except StopIteration as e:
print("100%")
fichier_obj.write(donnees)
objet_md5.update(donnees)
else:
print("--->réception du fichier terminée<---")
fichier_obj.close()
valeur_md5 = objet_md5.hexdigest()
md5_serveur = self.obtenir_reponse()
if md5_serveur['code_statut'] ==258:
if md5_serveur['md5'] == valeur_md5:
print("Vérification d'intégrité réussie pour %s!" %nom_fichier_base)
else:
progression = self.afficher_progression(reponse['taille_fichier'])
progression.__next__()
while taille_recue < reponse['taille_fichier']:
donnees = self.socket.recv(4096)
taille_recue += len(donnees)
fichier_obj.write(donnees)
try:
progression.send(len(donnees))
except StopIteration as e:
print("100%")
else:
print("--->réception du fichier terminée<---")
fichier_obj.close()
def _envoyer(self,liste_commandes):
'''Méthode d'envoi de fichiers'''
print("envoyer--", liste_commandes)
if len(liste_commandes) == 1:
print("aucun nom de fichier spécifié...")
return
en_tete_client = {
'action':'envoyer',
'nomfichier':liste_commandes[1],
}
self.socket.send(json.dumps(en_tete_client).encode())
self.socket.recv(1)
fichier_obj = open(liste_commandes[1],'rb')
for ligne in fichier_obj:
self.socket.send(ligne)
if __name__ == '__main__':
ftp = ClientFTP()
ftp.interactif()