Voir le flux RSS

nothus

[Actualité] [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs

Noter ce billet
par , 17/01/2017 à 13h03 (646 Affichages)
Souvent, pour développer un site ou un service, on a besoin d'un petit script offrant l'usage d'un serveur sur lequel on ajoute très rapidement des fonctionnalités. L'objectif est de tester des idées ou de développer des parties sans recourir à tout un ensemble plus vaste. C'est l'objet du code du jour...

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 ) :


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 - Visualiser dans une fenêtre à part
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()

Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Viadeo Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Twitter Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Google Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Facebook Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Digg Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Delicious Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog MySpace Envoyer le billet « [Python] Associer facilement des actions à vos requêtes HTTP grâce aux décorateurs » dans le blog Yahoo

Mis à jour 18/01/2017 à 09h48 par Nothus (Correction d'un bug)

Catégories
Développement Web , Programmation , Python

Commentaires