Avant de lire le code suivant, gardez à l'esprit qu'il n'a d'intérêt qu'à la condition de comprendre l'un des outils les plus puissants de Python : les décorateurs. Il s'agit de l'encapsulation d'une fonction dans une autre, permettant en quelque sorte de la "surcharger" dans modifier son contenu initial (utile pour traiter les données en entrée ou en sortie, pour le log ou comme ici, pour avoir une référence de fonction dans un dictionnaire en propriété de classe).
Plus d'information sur l'excellent blog de 'Sam et Max' (dont je suis un lecteur assidu ) :
- http://sametmax.com/comprendre-les-d...-pas-partie-1/
- http://sametmax.com/comprendre-les-d...-pas-partie-2/
- http://sametmax.com/creer-un-decorateur-a-la-volee/
N'hésitez pas à commenter si vous voyez des erreurs et / ou des améliorations. Attention, ne pas utiliser ce code au-delà d'une logique de développement, il n'est ni optimisé ni particulièrement sécurisé (surcouche SSL, etc).
MàJ 18/01/17 : correctif d'une erreur de programmation pour 'Service.get_request' et ajout d'une couche SSL
Bonne lecture !
---
Code python : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | import http.server import socket import threading import socketserver import re import ssl # --- nothus serv # --- Julien Garderon, janvier 2017 """(0) - A quoi ça sert ? Cet exemple (qui peut facilement être déployé sous forme d'un module) permet d'avoir des décorateurs prêts à l'emploi pour faciliter l'intégration de fonctions pour un serveur TCP gérant des requêtes HTTP classiques. Son fonctionnement est simple : le décorateur lie une fonction à une action, qui est appelée si : - le client est autorisé à se connecter (sinon la connexion est rompue) - PUIS le domaine correspond à l'expression régulière - PUIS une action dans les entêtes de la requête est trouvée, ou une action par défaut (sinon c'est une erreur 500 qui est renvoyée). L'utilisation des entêtes permet de garder la gestion du PATH de l'URL au sein de la fonction (appelé dans mon cas "action") ainsi que de détecter les 'upgrades' de la connexion comme le prévoit par exemple l'utilisation des WebSockets par la méthode GET : il suffit de changer de 'Requete._entete_action' et de valider la poignée de main dans une fonction dédiée, qui éventuellement peut instancier un objet déterminé gérant la connexion WebSocket par la suite. """ """ (1) - La classe Service La classe Service sert à "rendre le service" : accepter les connexions et les basculer vers le 'handler' (la requête en bon français), c'est-à-dire une classe instanciée par connexion qui va comprendre et résoudre la demande transmise. La classe Service est créé en héritant à la fois d'une possibilité de 'threading' et des fonctions de la classe serveur TCP par Python. J'ai écrasé la méthode 'get_request' qui accept la connexion. Dès celle-ci acceptée, on compare l'IP et le port utilisé aux plages possibles, qui peuvent être ajoutés avant le lancement du service (par 'httpd.serve_forever', ou httpd est la variable comprenant la classe Service instanciée). Ces plages peuvent être d'autorisation 'True' ou 'False' (toute valeur différente sera considérée comme False) et se déterminent comme une expression régulière : '(.*)' acceptera donc toutes les connexions et '127\.0\.0\.1\:([0-9]+)' seulement l'adresse locale. La plage des IP est utiles notamment pour l'approche réseaux locaux ou pour bloquer certaines plages correspondant à des IP étrangères non-souhaitées. """ class Service(socketserver.ThreadingMixIn, socketserver.TCPServer): _plagesIP = {} _plageDefaut= False def get_request(self): request, client_address = self.socket.accept() if self._plageDefaut==False: c = ":".join(map(str,client_address)) r = False for e in self._plagesIP: e = self._plagesIP[e] if re.match(e[0],c)!=None: r = e[1] if r!=True: print("connexion refusée pour : "+c) self.close_request(request) raise OSError() return request = ssl.wrap_socket( # retirer cette fonction pour ne pas disposer d'une connexion SSL/TLS request, server_side=True, certfile = "./certificate.crt", # à personnaliser keyfile = "./privatekey.key", # à personnaliser ssl_version = ssl.PROTOCOL_TLS # cette valeur, qui définit la meilleure possibilité de cryptage, est spécifique à Python 3.6 ) return (request,client_address) def _plageIP(self,expRegIP,autorisation): try: autorisation = True if autorisation==True else False self._plagesIP[expRegIP] = (re.compile(expRegIP),autorisation) except: pass """(2) - La classe Requete Il est possible d'ajouter des actions en utilisant le décorateur Python '@Requete._action' où les paramètres sont : - la liste des domaines possibles pour cette fonction, - la méthode (GET, POST, HEAD, etc) où par défaut seules les méthodes GET, POST et HEAD sont gérées (mais on peut rajouter PUT, ...), - le nom de l'action, qui se retrouve ensuite dans l'entête de la requête (si une valeur n'est pas trouvée, c'est le texte "defaut" qui est utilisé). Ainsi une requête GET HTTP classique d'un navigateur web peut être gérée avec le décorateur : >> @Requete._action((r"(.*)",),"GET","defaut") [ suivie d'une fonction ] nb : - le décorateur annule la fonction passée au décorateur, qui retournera systématiquement None ; - si une action est trouvée, la boucle s'arrête : les actions les plus "larges" doivent être déclarées grâce au décorateur en dernier ; - la fonction définissant une action doit avoir toujours DEUX paramètres : l'objet de la requête ('self') et le résultat de la recherche régulière du domaine. """ class Requete(http.server.SimpleHTTPRequestHandler): _entete_action = "nothus-action" _domaines = {} _actions = {} def _executer(self,action_type): try: domaine_id = False d_voulu = self.headers.get("host") for d in self._domaines: d_r = re.match(self._domaines[d],d_voulu) if d_r!=None: domaine_id = d domaine_resultat = d_r break if domaine_id==False: self.send_response(404) return a = self.headers.get(self._entete_action) if a is None: self._actions[domaine_id][action_type]["defaut"](self,domaine_resultat) else: self._actions[domaine_id][action_type][a](self,domaine_resultat) except: self.send_response(500) return def do_HEAD(self): return self._executer("HEAD") def do_GET(self): return self._executer("GET") def do_POST(self): return self._executer("POST") @classmethod def _action(self,domaines,action_type,action_id): def deco(fct): for domaine in domaines: try: self._domaines[domaine] except: try: self._domaines[domaine] = re.compile(domaine) except: print("Un domaine a rencontré un erreur lors de la compilation") pass try: self._actions[domaine] except: self._actions[domaine] = {} try: self._actions[domaine][action_type] except: self._actions[domaine][action_type] = {} self._actions[domaine][action_type][action_id] = fct def wrapper(): return return wrapper return deco ## Partie 1 : gérer les actions if __name__=="__main__": ## --> Gère l'appel du local (attention, c'est la valeur HOST de l'enête qui est utilisée : il faut confirmer qu'il s'agit bien du local avec l'IP) @Requete._action((r"^localhost\:8000$",),"GET","defaut") def requete_action_defaut(self,domaine_resultat): self.send_response(200) self.send_header('nothus-retour','pouet') self.send_header('Content-type','text/html') self.end_headers() self.wfile.write( ("Vous avez demandé "+domaine_resultat.group(0)).encode("utf-8") ) return ## --> Gère l'appel de toutes les pages GET n'ayant pas d'action particulière dans la requête pour tous les domaines @Requete._action((r"(.*)",),"GET","defaut") def requete_action_defaut(self,domaine_resultat): """Serve a GET request.""" # -> c'est la fonction par défaut de la doc officielle de Python 3.6 pour mon cas f = self.send_head() if f: try: self.copyfile(f, self.wfile) finally: f.close() ## --> Gère l'appel de toutes les pages GET ayant pour action particulière dans la requête pour tous les domaines la valeur "pouet" @Requete._action((r"(.*)",),"GET","pouet") def requete_action_defaut(self,domaine_resultat): self.send_response(200) self.send_header('nothus-retour','pouet-pouet') self.send_header('Content-type','text') self.end_headers() self.wfile.write(b"Hello World !") return ## Partie 2 : lancer le service if __name__=="__main__": with Service( ("", 8000), Requete ) as httpd: ## ... accepter toutes les IP (car, par défaut, la valeur de l'autorisation si l'IP ne correspond à aucune plage, est False) httpd._plageIP( r"(.*)", True ) print("serving at port", 8000) httpd.serve_forever() |