#! /usr/bin/env python # -*- coding: iso-8859-15 -*- # Ce script est un serveur SSL permettant aux bornes d'exécuter # certains scripts. # Les scripts sont découpés en deux catégories : # - ceux exécutés au démarrage de la borne # - ceux à exécuter au plus tôt (pour une mise à jour) # Ils se trouvent dans /etc/wifi/wifi-update (ou dans le contenu de la # variable ROOT). Ce répertoire contient plusieurs sous-répertoires. Chaque # sous-répertoire correspond et est nommé selon le nom de la borne # (ex : valhalla.wifi.crans.org). # Dans chacun de ces répertoires, il doit y avoir des scripts qui commencent # par 0 et des scripts qui commencent par un autre chiffre. Les scripts # qui commencent par 0 sont ceux exécutés au démarrage de la borne et les autres # sont ceux devant être mis à jour. # Comme un certain nombre de bornes partagent les même scripts, ceux-ci pourront # être des liens symboliques. Enfin, si une borne n'est pas présente dans # l'arborescence, on utilise le répertoire "unknown". # Le serveur accepte trois commandes : # BOOT qui permet d'obtenir le script de boot # UPDATE qui permet d'avoir le script de mise à jour # RESET qui permet de dire que l'on a bien reçu le script de mise à jour # et qu'il peut donc être effacé sur le serveur # La transmission d'un script se termine par un "." isolé sur une ligne. # La création des scripts et leur gestion est laissée à un programme externe. # Lancement : twistd -n -y wifi-update.py --pidfile=/var/run/wifi-update.pid # Pas de -n pour qu'il passe en fond # TODO: meilleure gestion des erreurs import time, os import commands,re # On utilise twisted # http://twistedmatrix.com/documents/current/howto/tutorial/intro from twisted.internet import protocol, reactor, defer, utils from twisted.internet.ssl import ContextFactory from twisted.protocols import basic from twisted.application import internet, service from twisted.python import log import sys, resource sys.path.append('/usr/scripts/gestion') # LDAP from ldap_crans import crans_ldap # Divers from iptools import AddrInNet from OpenSSL import SSL import re, errno, tempfile from stat import * class ServerContextFactory(ContextFactory): def getContext(self): """Création d'un contexte SSL côté serveur.""" ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_certificate_file('/etc/ssl/certs/nectaris.pem') ctx.use_privatekey_file('/etc/ssl/private/nectaris.pem') return ctx class UpdateProtocol(basic.LineReceiver): """Protocole de communication pour mettre à jour une borne wifi. Trois commandes sont possibles : BOOT qui permet d'obtenir le script de boot UPDATE qui permet d'obtenir le script de mise à jour RESET qui permet de confirmer la bonne réception de ce dernier """ def lineReceived(self, ligne): ligne = ligne.strip() if ligne == 'BOOT': self.do_BOOT() elif ligne == 'UPDATE': self.do_UPDATE() elif ligne == 'RESET' or ligne == 'ACK': self.do_RESET() else: self.sendError("`%s' is an unknown command for this server." % ligne) def sendError(self, error): """Renvoie une erreur sous forme de script.""" if error.__class__.__name__ != 'str': error = error.getTraceback() for ligne in error.split("\n"): self.sendLine("# %s" % ligne) self.sendLine(".") # Le plus simple est de couper la connexion self.transport.loseConnection() # On loggue aussi niveau serveur print error def do_BOOT(self): """Répond à une commande de type BOOT""" d = self.factory.getBootScripts(self.transport.getPeer().host) d.addCallback(self.sendScript) d.addErrback(self.sendError) def do_UPDATE(self): """Répond à une commande de type UPDATE""" d = self.factory.getUpdateScripts(self.transport.getPeer().host) d.addCallback(self.sendScript) d.addErrback(self.sendError) def do_RESET(self): """Répond à une commande de type RESET""" d = self.factory.reset(self.transport.getPeer().host) d.addCallback(self.sendScript) d.addErrback(self.sendError) def sendScript(self, script): """Renvoie un script arbitraire.""" if len(script) > 0: for ligne in script.split("\n"): if ligne != ".": self.sendLine(ligne) else: self.sendLine("..") else: self.sendLine("") self.sendLine(".") class UpdateFactory(protocol.ServerFactory): """Backend du serveur. Fournit les scripts.""" protocol = UpdateProtocol BASE = "/etc/wifi/wifi-update" def reset(self, host): """Efface le contenu éventuel du répertoire retry. Il n'y a pas de lock mis en place pour cette opération : aucun processus externe n'est censé écrire là-dedans.""" def reset_host(self, host): # ETAPE 1 # On commence par résoudre "host". return defer.succeed(reset_del(self, RE.get_reverse(host))) def reset_del(self, host): # ETAPE 2 # On efface try: os.chdir('%s/%s' % (self.BASE, host)) except OSError, e: if e.errno != errno.ENOENT or host == "unknown": raise os.chdir('%s/%s' % (self.BASE, "unknown")) # On regarde si le répertoire retry existe if not os.path.isdir('retry'): os.mkdir('retry') for f in os.listdir('retry'): try: os.remove('retry/%s' % f) except OSError: pass return "" # Aucun script à retourner return reset_host(self,host) def getBootScripts(self, host): """Retourne les scripts de boot. Les scripts commençant par 0 sont sélectionnés. Lorsque l'on accuse réception de la commande, ce seront les scripts de mise à jour présents lors de la commande qui seront effacés car ils sont considérés comme inclus dans les scripts de démarrage. """ return self.getScriptsAndDelete("^0.*[^~]$", "^[1-9].*[^~]$", host) def getUpdateScripts(self, host): """Retourne les scripts de mise à jour.""" return self.getScriptsAndDelete("^[1-9].*[^~]$", "^[1-9].*[^~]$", host) def getScriptsAndDelete(self, getre, delre, host): """Retourne un ensemble de scripts et enregistre à la suppression un autre ensemble. Les scripts correspondant à l'IP `host' et à l'expression régulière `getre' sont concatanés dans l'ordre et renvoyé et ceux correspondant à l'expression régulière `delre' sont marqués à la suppression. En pratique, c'est un peu plus compliqué. Les scripts communs à delre et getre sont placés dans un sous-répertoire retry, ceux qui sont uniquement dans delre sont effacés. Si le répertoire retry n'est pas vide, son contenu est envoyé et rien d'autre n'est fait. """ def getSAD_host(self, getre, delre, host): # ETAPE 1 # On commence par résoudre "host". return getSAD_lock(self, getre, delre, RE.get_reverse(host)) def getSAD_lock(self, getre, delre, host): # ETAPE 2 # Plus de lock pour le moment... d = defer.succeed(None) d.addCallback(lambda _: getSAD_script(self, getre, delre, host)) return d def getSAD_script(self, getre, delre, host): # ETAPE 3 # On fait le reste du boulot et on retourne le script à exécuter try: os.chdir('%s/%s' % (self.BASE, host)) except OSError, e: if e.errno != errno.ENOENT or host == "unknown": raise os.chdir('%s/%s' % (self.BASE, "unknown")) # On regarde si le répertoire retry existe if not os.path.isdir('retry'): os.mkdir('retry') if os.listdir('retry'): # Le répertoire retry n'est pas vide result = "" # On va concaténer ces fichiers et en renvoyer le contenu # Il ne devrait n'y en avoir qu'un, mais... for f in os.listdir('retry'): result = result + file('retry/%s' % f).read() # remove_lock('gen_confs.wifi') return result else: # Il n'y a rien dans le répertoire retry result = "" getre = filter(lambda f: re.match(getre, f), os.listdir(".")) delre = filter(lambda f: re.match(delre, f), os.listdir(".")) # On s'occupe de mettre ce qu'il faut dans retry both = filter(lambda f: f in delre, getre) both.sort() for f in both: result = result + file(f).read() # On écrit result dans un fichier temporaire dans retry retry = os.fdopen(tempfile.mkstemp('','','retry')[0],'w') retry.write(result) del result # On s'occupe de construire le résultat result = "" getre.sort() for f in getre: result = result + file(f).read() # On efface ce qui correspond à delre for f in delre: os.remove(f) # remove_lock('gen_confs.wifi') if len(result) > 5: print "We send the following script to %s :" % host print result return result return getSAD_host(self, getre, delre, host) # Corps du programme class ReverseEngine: def __init__(self): self.iphost = None self.lastupdate = 0 def get_reverse(self,ip): def build_reverse(server="138.231.144.3"): """Construit une correspondance IP -> nom""" pattern = re.compile("^(.*).in-addr.arpa domain name pointer (.*)\.") result = {} for line in commands.getoutput("host -l 148.231.138.in-addr.arpa %s" % server).split("\n"): mo = pattern.match(line.strip()) if mo: ip = ".".join(mo.group(1).split(".")[::-1]) result[ip] = mo.group(2) return result current = time.time() if not self.iphost or current - self.lastupdate > 60: self.iphost = build_reverse() self.lastupdate = current if ip not in self.iphost: return "unknown" else: return self.iphost[ip] RE = ReverseEngine() # On augmente la limite soft (qui semble arreter python). # Sous Open, on peut utiliser fstat pour voir les fichiers ouverts. # On a besoin de 3 * nombre de bornes pour le nombre de descripteurs # de fichiers : 1 descripteur pour le stream TCP et deux descripteurs # pour la crypto. limite = resource.getrlimit(resource.RLIMIT_NOFILE) resource.setrlimit(resource.RLIMIT_NOFILE, (300, limite[1])) # On écoute sur le port 9999 application = service.Application('wifi-update') factory = UpdateFactory() server = internet.SSLServer(9999, factory, ServerContextFactory()) server.setServiceParent(service.IServiceCollection(application))