IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Décorateurs: associer facilement des actions à vos requêtes HTTP

Le , par Nothus

0PARTAGES

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

Une erreur dans cette actualité ? Signalez-nous-la !