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