Comment faire un serveur de chat sécurisé avec Twisted et un zeste de PyOpenSSL.
C'est parti :
from twisted.internet.protocol import ServerFactory
from twisted.internet import reactor, ssl # pour ssl.ContextFactory
from OpenSSL import crypto, SSL #pour générer le contexte, la clé, et le certificat
from twisted.protocols import basic
import os
class MonFactoryServeur(ServerFactory):
def __init__(self):
self.protocol = MonProtocole
self.connectes = []#la liste des connectés
if __name__ == '__main__':
monFactory = MonFactoryServeur()
Comme d'habitude, on commence par créer le factory de notre serveur ( le factory va instancier le protocole auquel il est lié et gérer sa configuration/évènements )
Serverfactory est identique à Factory, elle sert juste à préciser que le factory (et donc le protocole lié) est à utiliser côté serveur.
MonFactoryServeur ne fait pas grand chose :
elle va utiliser MonProtocole( que l'on verra ensuite ),
elle contient une liste : connectes, qui va contenir les utiliseurs connectés sur notre serveur .
Maintenant notre serveur va devenir un serveur TLS .
Pour se faire, on rajoute ceci à la suite, dans le '__main__' :
reactor.listenSSL(2222, monFactory, MonContexte() )# se sert de twisted.internet.ssl.Server afin de faire un serveur ssl
reactor.run()
reactor va catcher tous les évènements se produisant sur un port donné (2222 en l'occurence).
listenSSL() va connecter monFactory (enfin MonProtocole) auport 2222. La connexion créée sera ainsi sécurisée (en TLS) via MonContexte.
Notre connexion sécurisée a donc besoin d'une clé privée ainsi que d'un certificat 'publique' .
C'est MonContexte qui va s'occuper de cela :
class MonContexte(ssl.ContextFactory):
def getContext(self):
"""seule méthode à surcharger"""
if not os.path.exists('cle.pem') or not os.path.exists('certificat.pem') :
cle = crypto.PKey()
cle.generate_key(crypto.TYPE_RSA, 1024)
certificat = crypto.X509()
certificat.gmtime_adj_notBefore(0)
certificat.gmtime_adj_notAfter( 60*60*24*365*1)
certificat.set_pubkey(cle)
certificat.sign(cle,'md5')
fichier = open('cle.pem','w+')
fichier.write( crypto.dump_privatekey(crypto.FILETYPE_PEM, cle) )
fichier.close()
fichier = open('certificat.pem','w+')
fichier.write( crypto.dump_certificate(crypto.FILETYPE_PEM, certificat) )
fichier.close()
monContexte = SSL.Context(SSL.TLSv1_METHOD) # les méthodes de notre objet Contexte :
monContexte.use_privatekey_file('cle.pem')
monContexte.use_certificate_file('certificat.pem')
return monContexte
MonContexte doit hériter de ContextFactory .
Elle ne contient qu'une seule méthode : getContext().
Dans cette méthode, si notre fichier cle.pem ou certificat.pem n'existe pas alors il faut recréer la clé et le certificat.
crypto.PKey() permet de créer notre objet cle de type PKey qui contiendra donc notre clé privée.
Pour la créer, on utilise la méthode generate_key() de cle.
Elle va en générer une de 1024 bits en utilisant l'algo RSA.
crypto.X509() permet de créer notre objet certificat qui sera de type X509 qui contiendra notre certificat 'public'.
Pour générer un certificat , il faut au minimum utiliser les méthodes :
gmtime_adj_notBefore() : la date à laquelle le certificat commence à être valide : 0 pour maintenant (en secondes [timestamp] ).
gmtime_adj_notAfter() : la date à laquelle le certificat arrête d'être valide (en secondes [timestamp] ).
set_pubkey() : va générer la clé publique de la clé privée : cle.
sign() : permet de signer le certificat en utilisant la clé privée ainsi qu'une méthode de hashage (md5, sha1)
Ensuite, on doit mettre notre cle ainsi que notre certificat dans des fichiers.
Pour cela, on utilise respectivement les fonctions dump_privatekey() et dump_certificate().
Elles prennent 2 arguments :
le format du fichier : FILETYPE_PEM : nos fichiers seront du format pem : ils vont contenir la cle/certificat.
la cle/certificat à utiliser.
Maintenant on peut créer monContexte via la classe Context qui prend en argument le type de notre connexion :
si on veut une connexion TLS alors ce sera TLSv1_METHOD, si on veut une connexion SSL alors on utilisera SSLv23_METHOD (ca va aussi influer sur les méthodes de monContexte, mais on s'en fou)
On indique à monContexte d'utiliser cle.pem et certificat.pem via les méthodes use_privatekey_file/use_certificate_file().
Voilà la méthode getContext() est terminée.
Maintenant on peut s'occuper de MonProtocole :
class MonProtocole(basic.LineReceiver):
def connectionMade(self):
print "Un client connecté"
self.factory.connectes.append(self)
def connectionLost(self, reason):
print "Un client perdu"
self.factory.connectes.remove(self)
def lineReceived(self, message):
print "Message reçu"
for client in self.factory.connectes:
client.sendLine(message + '\n')
MonProtocole hérite de LineReceiver.
MonProtocole a un attribut factory qui contient la seule et unique instance de monFactory ; et a seulement 3 méthodes :
Lorsqu'un client se connecte sur le serveur : connectionMade() est appelée.
Elle ajoute dans la liste connectes demonFactory (cf le debut) une instance de MonProtocole :
chaque client sera donc relié au serveur par une instance différente de MonProtocole (mais il n'y aura qu'une seule et unique instance de monFactory ).
Quand un client se déconnecte ou autre truc du genre , connectionLost() est appelée.
connectes supprime l'instance de MonProtocole du client perdu.
Quand un client envoie un message, lineReceived() est appelée.
Par l'intermédiaire de connectes on va envoyer à chaque client le message via sendLine().
Voilà notre serveur de chat sécurisé est enfin fini.
Place à la partie client:
from OpenSSL import SSL
import sys
from twisted.internet.protocol import ClientFactory
from twisted.internet import ssl, reactor, protocol, stdio
from twisted.protocols import basic
class MonFactoryClient(ClientFactory):
def __init__(self, pseudo):
self.protocol = MonProtocole
self.pseudo = pseudo
def clientConnectionFailed(self, connector, reason):
print 'Echec de connexion'
reactor.stop()
defclientConnectionLost(self, connector, reason):
print 'connexion perdue'
reactor.stop()
if __name__ == '__main__':
monFactory = MonFactoryClient('david')
monContexte = ssl.ClientContextFactory()
monContexte.method = SSL.TLSv1_METHOD
reactor.connectSSL('localhost', 2222, monFactory, monContexte)
reactor.run()
MonFactoryClient est basique (ClientFactory a le même rôle que le ServerFactory) :
elle prend en paramètre pseudo : qui sera le pseudo du client ;
son protocole lié est MonProtocole (on le verra ensuite).
Si la connexion au serveur a échouée alors clientConnectionFailed() est appelée, et le programme s'arrête.
Si la connexion au serveur est perdue alors clientConnectionLost() est appelée, et le programme s'arrête.
Après avoir instancier MonFactoryClient, on établit une connexion sécurisée à notre serveur via connectSSL(), qui prend 4 arguments:
l'adresse de notre serveur
le port sur lequel se connecter
monFactory (et donc MonProtocole )
monContexte .
monContexte contient la configuration de la connexion sécurisée,
c'est pourquoi il faut mettre l'attribut method de monContexte à : TLSv1_METHOD(vu que l'on veut établir une connexion TLS).
Voilà il ne reste plus qu'à créer notre classe MonProtocole:
class MonProtocoleSortie(protocol.Protocol):
def __init__(self, monProtocole):
self.monProtocole = monProtocole
def dataReceived(self, message):
message = self.monProtocole.factory.pseudo + ' dit : ' + message
self.monProtocole.sendLine(message)
class MonProtocole(basic.LineReceiver):#recoit les données et les renvoies dans self.output
def connectionMade(self):
print('Bienvenue ' + self.factory.pseudo)
self.sortie = stdio.StandardIO( MonProtocoleSortie(self) ) # doit être appelé avec protocol.Protocol
def lineReceived(self,message):
print message
MonProtocole hérite de LineReceiver.
Une fois que la connexion est établie , connectionMade() est appelée.
Cette méthode ne fait qu'une chose :
elle instancie la classe StandardIO() .
StandardIO prend un paramètre : une [autre] classe qui hérite de Protocol : MonProtocoleSortie.
Elle va connecter MonProtocoleSortie à l'entrée/sortie standard.
Or, MonProtocoleSortie a une seule méthode dataReceived().
Cela veut dire que c'est seulement l'entrée qui va être catchée par MonProtocoleSortie.
Donc quand le client va écrire un message sur sa console, dataReceived() va être appelée.
Elle va formater le message, et l'envoyer au serveur via la méthode sendLine() de MonProtocole .
Or quand le serveur envoie un message, c'est lineReceived() de MonProtocole qui va catcher cet évènement, pour l'afficher.
Voilà, voilà c'est enfin terminé .
Voici les 2 fichiers :
serveur.py:
#-*- coding:Utf-8 -*-
from twisted.internet.protocol import ServerFactory
from twisted.internet import reactor, ssl # pour ssl.ContextFactory
from OpenSSL import crypto, SSL #pour générer le contexte, la clé, et le certificat
import os
from twisted.protocols import basic
class MonProtocole(basic.LineReceiver):
def connectionMade(self):
print "Un client connecté"
self.factory.connectes.append(self)
def connectionLost(self, reason):
print "Un client perdu"
self.factory.connectes.remove(self)
def lineReceived(self, message):
print "Message reçu"
for client in self.factory.connectes:
client.sendLine(message + '\n')
class MonFactoryServeur(ServerFactory):
def __init__(self):
self.protocol = MonProtocole
self.connectes = []#la liste des connectés
class MonContexte(ssl.ContextFactory):
""" doit hériter
contient seulement getContext()"""
def getContext(self):
"""seule méthode à surcharger"""
if not os.path.exists('cle.pem') or not os.path.exists('certificat.pem') :
cle = crypto.PKey()
cle.generate_key(crypto.TYPE_RSA, 1024)
certificat = crypto.X509()
certificat.gmtime_adj_notBefore(0)
certificat.gmtime_adj_notAfter( 60*60*24*365*5)
certificat.set_pubkey(cle)
certificat.sign(cle,'md5')
fichier = open('cle.pem','w+')
fichier.write( crypto.dump_privatekey(crypto.FILETYPE_PEM, cle) )
fichier.close()
fichier = open('certificat.pem','w+')
fichier.write( crypto.dump_certificate(crypto.FILETYPE_PEM, certificat) )
fichier.close()
monContexte = SSL.Context(SSL.TLSv1_METHOD) # les méthodes de notre objet Contexte :
monContexte.use_privatekey_file('cle.pem')
monContexte.use_certificate_file('certificat.pem')
return monContexte
if __name__ == '__main__':
monFactory = MonFactoryServeur()
reactor.listenSSL(2222, monFactory, MonContexte())# se sert de twisted.internet.ssl.Server afin de faire un serveur ssl
reactor.run()
client.py:
#-*- coding:Utf-8 -*-
from OpenSSL import SSL
import sys
from twisted.internet.protocol import ClientFactory
from twisted.internet import ssl, reactor, protocol, stdio
from twisted.protocols import basic
class MonProtocoleSortie(protocol.Protocol):
def __init__(self, monProtocole):
self.monProtocole = monProtocole
def dataReceived(self, message):
print "un"
message = self.monProtocole.factory.pseudo + ' dit : ' + message
self.monProtocole.sendLine(message)
class MonProtocole(basic.LineReceiver):#recoit les données et les renvoies dans self.output
def connectionMade(self):
print('Bienvenue ' + self.factory.pseudo)
self.sortie = stdio.StandardIO( MonProtocoleSortie(self),1,1 ) # doit être appelé avec protocol.Protocol def lineReceived(self,message):
print "deux"
print message
class MonFactoryClient(ClientFactory):
def __init__(self, pseudo):
self.protocol = MonProtocole
self.pseudo = pseudo
def clientConnectionFailed(self, connector, reason):
print 'Echec de connexion'
reactor.stop()
def clientConnectionLost(self, connector, reason):
print 'connexion perdue'
reactor.stop()
if __name__ == '__main__':
monFactory = MonFactoryClient('david')
monContexte = ssl.ClientContextFactory()
monContexte.method = SSL.TLSv1_METHOD
reactor.connectSSL('localhost', 2222, monFactory, monContexte)
reactor.run()