Magazine Internet

python, SSH et twisted

Publié le 15 août 2008 par Mikebrant

Créer une connexion ssh en python avec twisted .

Enfin un article qui ne porte pas sur Qt.

On va donc se servir de Twisted pour créer notre petite connexion ssh .

Twisted est assez spécial puisque c'est un event-driven framework, c'est à dire que l'on ne va pas faire appel, selon une séquence bien définie, à nos méthodes mais ce sont les évènements externes qui vont appelés telle ou telle méthode .
C'est un peu bizarre au début mais on s'y fait .

Ces évènements sont catchés par une boucle infinie, vial'objet reactor en lancant run() .
Une fois un évènement catché, la boucle va déclencher la méthode qui gère cet évènement .

On va tout d'abord créer notre fichier index.py qui va être notre main .

#-*- coding:Utf-8 -*-
from twisted.internet import reactor
import sys
from factory import MonFactory
if __name__=='__main__':
   
  if len(sys.argv)!=5 : #  5 et non pas 4 puisque le nom du fichier  est  un paramètre, mais on s'en fou . 
  print("Il faut 4 paramètres.\nUsage : python index.py monServeur monPort monIdentifiant monMotDePasse\n")
  exit(-1)
  monFactory = MonFactory(sys.argv[3], sys.argv[4])
  reactor.connectTCP(sys.argv[1], int( sys.argv[2] ), monFactory)
  reactor.run()#appelle verifyHost et connectionSecure


On doit lancer le programme avec  4 paramètres :
adresse du serveur,
port sur lequel on souhaite se connecter (ssh = 22 ) ,
le nom de l'utilisateur
et son mot de passe.

Ensuite,on instancie notre classe MonFactory , avec en paramètre l'utilisateur et le mot de passe.

MonFactory , qui hérite de protocol.ClientFactory gère les évènements liés à la connexion :
connexion perdue, connexion réussie, connexion échouée , etc ....

Après, on souhaite établir une connexion TCP (vu que l'on veut faire du ssh) vers notre serveur via le bon port , en n'oubliant pas notre factory, qui va donc la gérer.


Voici le code de monFactory :

#-*- coding:Utf-8 -*
from twisted.internet import reactor, protocol
from initialisation import Initialisation
class MonFactory(protocol.ClientFactory):
    
   def __init__(self,monUtilisateur,monMotDePasse):
   self.monUtilisateur = monUtilisateur
   self.monMotDePasse = monMotDePasse
   print('MonFactory')
    
    
   def buildProtocol(self, addr):
   " crée un objet de type protocol qui va gérer le flux d'entrée sur le serveur"
   print('MonFactory - buildProtocol')
   monProtocole = Initialisation(self.monUtilisateur, self.monMotDePasse)
      return monProtocole
    
   def clientConnectionLost(self, connecteur, message):  
   """ quand la connexion est perdue """
   print('Fin de la connexion : ' + message.getErrorMessage() )  
  
 
   def clientConnectionFailed(self, connecteur, message):
   """ quand la connexion a échouée """  
   print('Echec de connexion : ' + message.getErrorMessage() )
   reactor.stop() 


On fait donc hériter MonFactory notre de ClientFactory . A son instanciation on prend donc l'utilisateur et le mot de passe.
Puis sa méthode buildProtocol est appelée tout de suite après afin d'établir la connexion SSH et tout ce qui en découle .
Si par exemple, on spécifie une mauvaise IP/port , ce n'est pas buildProtocol qui sera appelée mais clientConnectionFailed .
clientConnectionLost  quant à lui, implique que buildProtocol ait déjà été appelée ; et il peut l'être quand on envoie un mauvais id/mdp .

On part du principe que tous nos paramètres sont bons , et que par conséquent buildProtocol est appelé .

Dans cette méthode,on fait pas grand chose : on instancie seulement notre classe Initialisation qui va s'occuper de la connexion SSH avec le serveur : vérification de l'empreinte du serveur, du certificat,chiffrement, ...  et si tout est bon l'authentification commence.

Dans notre exemple, on ne se connecte pas en ssh via certificat, et on zappe l'empreinte du serveur.
Voici donc ce que donne la classe :

#-*- coding:Utf-8 -*
from twisted.conch.ssh import transport
from twisted.internet import defer
from authentification import Authentification
from connexion import Connexion
class Initialisation(transport.SSHClientTransport):
   """  chiffrement et vérification """
  
  
   def __init__(self, monUtilisateur, monMotDePasse):
   self.monUtilisateur = monUtilisateur
   self.monMotDePasse = monMotDePasse
   print('Initialisation')
  
  
   def verifyHostKey(self,clePublique,empreinte):
   """à implanter ,
   clePublique : la clé publique envoyée par le serveur
   empreinte : l'empreinte du serveur
   """
   #on fait pas de vérif de clé ou empreinte
      print('Initialisation -verifyHost')
   return defer.succeed(True)# on accepte
  
  
  
   def connectionSecure(self):
   """ à implanter elle aussi
  
   """
      print('Initialisation - connectionSecure')  
      self.requestService(Authentification(self.monUtilisateur,self.monMotDePasse,Connexion() ) )


Alors, la première chose, que vous pouvez remarquer c'est que dans le __init__() il n'y a pas :
transport.SSHClientTransport.__init__()
Tout simplement parce que la méthode __init__() n'est pas implantée dans la classe Mère .
La méthode verifyHostKey() vérifie l'empreinte/le certificat du serveur. Ici comme on s'en fou, on retourne un deferred qui aura un callback 'positif' , via la méthode succeed() de defer .

Et là, c'est le drame ! vous ne comprenez plus rien, mais ne vous inquiétez pas je vais tout vous expliquer .
Notre programme marche de manière asynchrone, logique n'est-ce pas ?
Ca veut dire que lorsque l'on va effectuer une action, celle-ci va mettre un certain temps avant de s'accomplir, mais cela s'effectuera en arrière plan (le programme continuera comme si de rien était, sans attendre le résultat ) .
Notre objet deferred est donc l'action asynchrone , et qui une fois accomplie, appelera son callback contenant le résultat .

Il ya 2 types de callback :
callback()
errback()
errback() est appelée  si l'action asynchrone que l'on effectue est pour gérer une erreur (par exemple si l'empreinte du serveur auquel on se connecte était différente de celle écrit dans le programme, mais on ne gère pas celà ).
On créera alors notre action asynchrone ( deferred ) via la méthode fail() de la classe Defer . J'appelle ca un callback 'négatif' .
callback() est appelée si l'action asynchrone que l'on effectue est normale (l'empreinte du seveur est la même que celle dans le programme, où comme là,on ne vérifie rien donc tout se passe bien, on veut passer à la suite) .
On créera alors notre action asynchrone ( deferred ) via la méthode succeed() de la classe Defer . J'appelle ca un callback 'positif' .

Revenons à nos moutons .
Après verifyHostKey() ,le chiffrement est établie et  connectionSecure() est appelée .
Elle va essayer de s'authentifier via la classe Authentification .
Le nom de classe dit à peu près tout ce qu'il faut savoir donc voici son code :

#-*- coding:Utf-8 -*
from twisted.conch.ssh import userauth
from twisted.internet import defer
class Authentification(userauth.SSHUserAuthClient):
  
   def __init__(self,monUtilisateur,monMotDePasse,maConnexion):
   userauth.SSHUserAuthClient.__init__(self,monUtilisateur,maConnexion)
   self.monMotDePasse = monMotDePasse
   print("Authentification")
  
  
  
   def getPassword(self, prompt = None):
   """ à implanter """
   print("Authentification - getPassword ")
   return defer.succeed(self.monMotDePasse)


Authentification prend trois arguments :
l'utilisateur , son mot de passe, et maConnexion qui va être la connexion sécurisée(vu après) .
La classe mère quand à elle ne prend pas en arguments le  mot de passe, puisque on est censé le tapper quand on arrive à ce stade, c'est pour cela qu'il y a la méthode getPassword() .
getPassword() a un argument : le prompt, que l'on met à None, puisqu'on ne veut pas tapper notre mot de passe (on le tappe quand on lance le programme ) .
La méthode retourne un callback 'positif' , vu que de toute manière, que le mot de passe soit mauvais ou pas, on ne peut que partir du principe que ce soit le bon pour essayer de passer à la suite .

maConnexion est l'instanciation de la classe Connexion , qui va gérer la connexion une fois l'authentification réussie . Voici son code :

#-*- coding:Utf-8 -*
from twisted.conch.ssh import connection
from channel import Channel
class Connexion(connection.SSHConnection):
   """ va gérer la connexion SSH """
  
   def serviceStarted(self):
   """ est appelé dès que l'authentification a réussie"""
   print("serviceStarted")
   self.openChannel(Channel(maCommande = 'ls' , conn = self))


Assez court .
Une fois que l'on est authentifié , la méthodeserviceStarted() de maConnexion( passée en paramètre ) est appelée .
Elle ne fait qu'une chose : elle ouvre un channel .
un  Channel va nous permettre de lancer des commandes, ici on souhaite lancer ls .
conn
  quand à lui est une clé d'un dictionnaire, il ne peut donc pas s'appeler autrement . Il permet de préciser dans quelle connexion on souhaite ouvrir le channel .

Et donc voici la classe Channel, qui va lancer la commande  :

#-*- coding:Utf-8 -*
from twisted.conch.ssh import channel, common
from twisted.internet import reactor
class Channel(channel.SSHChannel):
   """ interface entre le client et le serveur
   ici, il va balancer des cat (mais pas les chats hein)
   """
   name = 'session'
  
   def __init__(self,maCommande,**kwargs):
      channel.SSHChannel.__init__(self,**kwargs)
      self.maCommande = maCommande  
   def channelOpen(self, donnees):
   self.donnees = ''
   deferred = self.conn.sendRequest(self, 'exec', common.NS(self.maCommande),wantReply = True)
   # self : il envoie la requete depuis ce channel.
   # exec : pour dire au serveur que l'on veut éxécuter une commande .
   #common.Ns : encode notre requete au format NetworkString
   # wantReply = True : pour avoir une réponse : la commande a démarrée.
   deferred.addCallback(self.monCallBack)
   def monCallBack(self, resultat):
   """ une fois la commande éxécutée"""
   self.conn.sendEOF(self)#on enverra plus rien
  
  
   def dataReceived(self, donnees):
   """ on recoit les données petit à petit"""
   self.donnees += donnees
     
   def closed(self):
   """ est appelé quand les 2 côtés ont fermé la connexion """
   print '\n\nLa commande nous renvoie ceci :\n\n' + self.donnees
      reactor.stop()


A l'initialisation, rien de bien compliqué (**kwargs  étant obligatoire, il représente le dictionnaire qui contient conn).
La méthode channelOpen() va envoyer une requête (la commande) via  la méthode sendRequest() de la valeur de conn.
Pour les arguments de cette méthode, lisez les commentaires. Elle renvoie un objet deferred puisqu'elle est asynchrone .
On ajoute donc un callback à notre objet : monCallBack() , qui sera appelé une fois la commande lancée .
monCallBack()  contient une seule instruction : sendEOF() qui va avertir le serveur que l'on enverra plus rien depuis ce channel .
dataReceveived() est appelée au fur et à mesure que l'on recoit des données dûes à la commande.

closed() est appelée quand le channel est clos, c'est à dire lorsque la commande est terminée et que l'on a recu toutes les données .

Voilà, c'est terminé, il peut subsister des fautes vu que j'ai pas le courage de me relire, je préfère aller dormir .
Si vous avez des questions n'hésitez pas.
ps : aucune blague, ni de référence musicale dans cet article, c'est impardonnable je le sais... mais je ferais mieux dans le prochain article .


Retour à La Une de Logo Paperblog

LES COMMENTAIRES (1)

Par bonjour94
posté le 13 novembre à 14:28
Signaler un abus

Merci pour cette illustration, Avez-vous un autre exemple ou vous avez essayé de vous connecté en ssh via des certificats x509??? Merci Pour votre réponse, [email protected]

A propos de l’auteur


Mikebrant 9 partages Voir son profil
Voir son blog

l'auteur n'a pas encore renseigné son compte l'auteur n'a pas encore renseigné son compte

Dossier Paperblog