= TP6 : Serveur HTTP minimaliste == Objectif == Le but du dispositif final est de créer un site web permettant d'accéder à des capteurs déportés. [[Image(htdocs:png/IOC6_but_principe.png,nolink,600px)]] c Il y au cœur du dispositif une machine, nommée ''station de base'', contenant un serveur HTTP et une application, nommée ''gateway'', permettant d'accéder aux capteurs distants via un réseau sans fil. Pour ce dispositif : la station de base doit être une !RaspberryPi 3 et les capteurs distants des ESP32. L'application ''gateway'' doit utiliser un protocole de communication nommé MQTT. En l'état actuel, vous n'allez pas pouvoir travailler sur les !RaspberryPi 3, en conséquence, la station de base sera un des PC de la salle SESI. Il n'y aura pas non plus d'ESP32 et donc, dans un premier temps l'application ''gateway'' produira des données synthétiques (les valeurs d'un compteur circulaire par exemple). Entre le serveur HTTP et ''gateway'' nous allons utiliser des FIFOs Unix. Nous verrons le protocole MQTT ultérieurement. [[Image(htdocs:png/IOC6_plateforme_TP.png,nolink,600px)]] Le browser web (p.ex. firefox) sera sur votre PC local, il devra communiquer avec un serveur HTTP sur votre PC du réseau enseignement. Nous allons devoir faire quelques manipulations pour rendre cela possible. En effet, nous avons deux obstacles. Le premier est que les machines du réseau enseignement ne sont pas directement accessibles, elles sont derrière une machine ''proxy'' (''durian''), le second est que le port d'écoute standard des serveurs web est 80 et que celui-ci est déjà utilisé par un serveur préinstallé (''apache''), nous devrons donc utiliser un autre port. Nous allons utiliser un tunnel ssh et de la redirection de port. == création du tunnel ssh == Je vais supposer que vous êtes sur une machine Unix (Linux ou MacOS). Je sais que certains d'entrevous sont sur Windows et ce que je vous demande de faire est possible sur Windows, mais, en l'état, je ne sais pas le faire. Je pourrai essayer si nécessaire en utilisant une machine virtuelle et un Windows10. Sur le schéma ci-dessous : * `localhost` : c'est votre machine chez vous * `durian`: c'est la machine passerelle par laquelle vous devez passer pour entrer dans le réseau enseignement. * `musicien`: c'est le nom d'une machine des salles 305-307, par exemple `mozart` * 2222, 8080, 22 et 8000 : sont des ports d'écoute utilisés par le serveur ssh et le serveur HTTP. [[Image(htdocs:png/IOC6_tunnel-ssh.png,nolink,600px)]] 1. Commençons par renseigner durian de votre clé publique ssh, afin que vous n'ayez pas à taper votre mot de passe à chaque fois que vous vous connectez sur `durian`. Vous avez déjà fait ça pour l'accès aux !RaspberryPi 1. Ce sera à faire une seule fois. Sur votre PC chez vous: {{{#!bash ssh-keygen -t rsa ssh-copy-id -i $HOME/.ssh/id_rsa.pub loginname@durian.lip6.fr }}} * Remplacer `loginname` par votre nom de login. * Pour la première commande, vous devez taper 3 fois sur la touche entrée pour choisir les valeurs par défaut. * Pour la deuxième commande, vous tapez votre mot de passe d'accès à la passerelle (ce sera la dernière fois). 2. Création des tunnels. Nous allons en créer deux pour traverser la passerelle. Malheureusement, ces opérations devront être réalisés à chaque fois.[[BR]]. * Vous ouvrez deux terminaux sur votre machine locale. Sur le premier terminal, vous taper la commande suivante: {{{#!bash ssh loginname@durian.lip6.fr -L 2222:musicien:22 }}} Ainsi, vous vous connectez sur durian (vous n'avez pas à taper votre mot de passe si vous avez bien fait l'étape 1.) et vous configurez un tunnel sur `durian` dont l'entrée est `localhost:2222` et la sortie est `musicien:22`, c'est-à-dire que ce que vous envoyez sur le port 2222 depuis votre machine chez vous, arrive sur le port 22 de la machine `musicien`. Vous ne devez pas vous déloguer de Durian, sinon vous cassez le tunnel, par contre, vous pouvez aller sur la machine `musicien` depuis ce terminal (vous pouvez refaire le 1. parce que je crois ce n'est pas exactement le même compte).[[BR]]. * Sur l'autre terminal, que vous n'aviez pas encore utilisé, vous tapez: {{{#!bash ssh -p 2222 localhost -L 8080:musicien:8000 }}} Vous faites un ssh sur `localhost` au port 2222 et grâce au premier tunnel, vos paquets vont être reçus par le serveur ssh de la machine `musicien` en passant à travers le proxy `durian`. Grâce à cette seconde commande, vous construisez un tunnel sur `musicien`, dont l'entrée est `localhost:8080` et la sortie est `musicien:8000`. Nous allons, par la suite, brancher un serveur web sur le port 8000 et pour lui envoyer vos requêtes vous aurez à mettre dans la case de l'URL `localhost:8080`, les requêtes arriveront sur le serveur en ayant transervées 2 tunnels.[[BR]]. * On aurait pu choisir d'autres numéros de port à la place de 2222 et 8080. J'ai choisi des numéros en rapport avec les numéros standard (22 pour ssh et 80 pour le serveur HTTP). Pour le port 8000, vous aurez peut-être à changer ce numéro, si vous êtes plusieurs sur la même machine du réseau enseignement. == Plan de la séance == Ce serveur web est écrit en Python, à la réception des requêtes du client, il exécute des scripts CGI (Common Gateway Interface) écrit également en Python pour produire des pages HTML dynamiques. Les scripts CGI devront communiquer avec le programme écrit en C. La communication entre les scripts et l'application se fera par fifo UNIX. Nous allons procéder en deux temps. 1. Nous allons faire communiquer un programme python avec un programme C par FIFO. 2. Nous allons créer un serveur local sur le PC de développement et le faire communiquer avec le programme C. == 1. Communication par FIFO == Pour démarrer, vous allez récupérer une [htdocs:docs/writer_reader.tgz archive] constituer de 4 fichiers: 2 lecteurs et 2 écrivains. Les deux lecteurs sont interchangeables, le premier est en C, le second en Python. Les deux écrivains sont aussi interchangeables. {{{ writer_reader ├── Makefile ├── reader.c : lit une fifo et affiche le message reçu jusqu'à recevoir le message end ├── reader.py : idem reader.c, mais en python ├── writer.c : écrit dans une fifo 5 fois et écrit le message end └── writer.py : idem writer.c, mais en python }}} Vous pouvez tester les programmes qui vous sont proposés. Vous ouvrez deux fenêtres sur la même machines. Vous n'êtes pas obligé d'utiliser une machine du réseau enseignement si vous êtes sur Linux. Vous démarrez un lecteur et un écrivain dans l'ordre que vous voulez. Vous serez sans doute amené à changer le nom des fifos si vous êtes sur des machines partagées puis que ces fifos sont créés dans `/tmp`. Je vous demande de lire les codes en commençant par les programmes python, en répondant aux questions suivantes. Si vous ne connaissez pas le langage [https://python.developpez.com/cours python] c'est le moment de vous y mettre, mais ce n'est rédhibitoire pour aujourd'hui. Ces questions ne sont pas exhaustives, l'idée c'est d'avoir une "''compréhension''" de ce qu’est dans le code (vous devrez utiliser Google pour ça). **writer.py** - Dans quel répertoire est créée la fifo ? - Quelle différence mkfifo et open ? - Pourquoi tester que la fifo existe ? - À quoi sert flush ? - Pourquoi ne ferme-t-on pas la fifo ? {{{#!python #!/usr/bin/env python import os, time pipe_name = '/tmp/myfifo' if not os.path.exists(pipe_name): os.mkfifo(pipe_name) pipe_out = open(pipe_name,'w') i=0; while i < 5: pipe_out.write("hello %d fois from python\n" % (i+1,)) pipe_out.flush() time.sleep(1) i=i+1 pipe_out.write("end\n") }}} **reader.py** - Que fait readline ? {{{#!python #!/usr/bin/env python import os, time pipe_name = '/tmp/myfifo' if not os.path.exists(pipe_name): os.mkfifo(pipe_name) pipe_in = open(pipe_name,'r') while str != "end\n" : str = pipe_in.readline() print '%s' % str, }}} Vous allez remarquer que lorsque le vous lancer un écrivain (en C ou en Python) rien ne se passe tant que vous n'avez pas lancé un lecteur. - Expliquez le phénomène. Regarder aussi le Makefile, et vérifiez que vous le comprenez. == 2. Création d'un serveur fake == Le but de cette première partie est de réaliser le programme suivant: [[Image(htdocs:png/fake2server.png,nolink,400px)]] - fake lit une valeur sur stdin et place la valeur lue dans une variable. - Lorsque l'on tape plusieurs valeurs de suite la nouvelle valeur écrase l'ancienne. - fake est toujours en fonctionnement. - fake attends aussi un message de la fifo s2f. - lorsqu'il reçoit un message, il l'affiche et il renvoie dans la fifo f2s la dernière valeur lue sur stdin. Vous commencez par récupérer l'[htdocs:docs/cgi_fake.tgz archive] qui donne un point de départ. {{{ fake ├── Makefile ├── cgi.py └── fake.c }}} - Dans un premier terminal, compilez et démarrez fake. - Dans un autre terminal, exécuter ./cgi.py - Le programme "cgi" Python est démarré et arrêté, il se comporte comme se comportera le script CGI exécuté par le serveur. - Quand le cgi python démarre, - il envoie un message sur la fifo s2f - puis il lit la fifo f2s et affiche le résultat. Vous devez : 1. modifier le select dans fake pour lire les deux fifos d'entrées stdin et s2f. 1. modifier cgi.py pour lire la valeur lue sur stdin afin que ce que cgi.py envoi ne soit pas "w hello", mais soit une chaine tapée au clavier. Le but est de "simuler" le comportement du script CGI et de vous obliger à écrire un peu de code Python. **fake.c** {{{#!c #include #include #include #include #include #include #include #define MAXServerResquest 1024 int main() { int f2s, s2f; // fifo file descriptors char *f2sName = "f2s"; // filo names char *s2fName = "s2f"; // char serverRequest[MAXServerResquest]; // buffer for the request fd_set rfds; // flag for select struct timeval tv; // timeout tv.tv_sec = 1; // 1 second tv.tv_usec = 0; // mkfifo(s2fName, 0666); // fifo creation mkfifo(f2sName, 0666); /* open both fifos */ s2f = open(s2fName, O_RDWR); // fifo openning f2s = open(f2sName, O_RDWR); do { FD_ZERO(&rfds); // erase all flags FD_SET(s2f, &rfds); // wait for s2f if (select(s2f+1, &rfds, NULL, NULL, &tv) != 0) { // wait until timeout if (FD_ISSET(s2f, &rfds)) { // something to read int nbchar; if ((nbchar = read(s2f, serverRequest, MAXServerResquest)) == 0) break; serverRequest[nbchar]=0; fprintf(stderr,"%s", serverRequest); write(f2s, serverRequest, nbchar); } } } while (1); close(f2s); close(s2f); return 0; } }}} **server.py** {{{#!python #!/usr/bin/env python import os, time s2fName = 's2f' if not os.path.exists(s2fName): os.mkfifo(s2fName) s2f = open(s2fName,'w+') f2sName = 'f2s' if not os.path.exists(f2sName): os.mkfifo(f2sName) f2s = open(f2sName,'r') s2f.write("w hello\n") s2f.flush() str = f2s.readline() print '%s' % str, f2s.close() s2f.close() }}} == 3. Création d'un serveur web == Nous allons maintenant créer le vrai server http. Dans l'[htdocs:docs/server_fake.tgz archive] vous trouvez un squelette de server que vous allez d'abord tester avant de l'exécuter le PC de la salle SESI {{{ server-fake ├── fake │   ├── Makefile │   └── fake.c └── server ├── server.py └── www ├── cgi-bin │   ├── led.py │   └── main.py ├── img │   └── peri.png └── index.html }}} **Pour tester le server http** - Dans un premier terminal, après l'avoir compilé, lancez le programme fake. Il s'agit du même programme fake.c que précédemment, qui reçoit une requête depuis une fifo s2f et qui renvoie une réponse, mais les fifos sont créées dans /tmp, il y a donc des risques de collision avec d'autres personnes. Si ca coince, changez le nom des fifos. {{{ cd fake make ./fake }}} - Dans un **second terminal**, lancez le server.py. C'est un server http en python. {{{ cd server/www ../server.py }}} - Si ce n'est pas déjà fait, installer les tunnels ssh et sur votre navigateur préféré, visualisez la page index.html à l'adresse `localhost:8080` - Vous devez voir apparaitre un logo et une case avec un bouton enter. - La page `index.html` contient deux "frames" (je sais les frames sont deprecated...) - Le premier avec le logo. - Le second est contient la case et le bouton. Le code html de cette case est obtenu par l'exécution du programme Python `cgi-bin/main.py`. - Notez qu'il n'est pas très utile d'avoir produit cette page par un programme python, car la page n'est pas dynamique (son code est toujours le même), mais c'est pour donner la possibilité de la rendre dynamique. - Lorsque vous écrivez quelque chose dans la case, la page index.html demande l'exécution de script `cgi-bin/led.py` - le script `led.py` envoi le contenu de la case sur la fifo `s2f` attendue par `fake` et produit une page presque identique à main.py avec deux différences. - Elle affiche ce qui a été reçu de la fifo `f2s` - Elle est remplacée au bout d'une seconde par la page `main.py` grace à une commande `` **server.py** Le server écoute le port 8000 et affiche la page index.htlm présente dans le répertoire wwww. {{{ X est une valeur entre 0 et 3, puisque nous allons avoir 4 serveurs HTTP par Raspberry. }}} {{{#!python #!/usr/bin/env python import BaseHTTPServer import CGIHTTPServer import cgitb; cgitb.enable() server = BaseHTTPServer.HTTPServer handler = CGIHTTPServer.CGIHTTPRequestHandler server_address = ("", 8000) handler.cgi_directories = ["/cgi-bin"] httpd = server(server_address, handler) httpd.serve_forever() }}} **index.html** {{{ Peri Web Server }}} **main.py** {{{#!python #!/usr/bin/env python html=""" Peri Web Server LEDS:
""" print html }}} **led.py** {{{#!python #!/usr/bin/env python import cgi, os, time,sys form = cgi.FieldStorage() val = form.getvalue('val') s2fName = '/tmp/s2f_fw' f2sName = '/tmp/f2s_fw' s2f = open(s2fName,'w+') f2s = open(f2sName,'r',0) s2f.write("w %s\n" % val) s2f.flush() res = f2s.readline() f2s.close() s2f.close() html=""" Peri Web Server LEDS:
set %s
""" % (val,) print html }}} == Travail à rendre == - résumer les expériences que vous avez réalisées et répondez aux quelques questions posées dans ce texte - Modifier `fake.c` en `capteur.c` et `led.py` pour afficher un message et qui lit la valeur courante d'un compteur qui s'incrémente chaque seconde. `led.py` s'appelle ainsi parce qu'au départ, il s'agissait de commander les leds de la !RaspberryPi 1. On va laisser ce nom, mais vous pouvez le changer si vous préférez. Il est probable que vous deviez utiliser les threads pour `capteur.c` afin d'avoir un thread qui s'incrémente chaque seconde qui écrit dans une variable globale que l'on peut lire quand une requête arrive.