wiki:IOC20_T06

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.

IOC6_but_principe.png 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.

IOC6_plateforme_TP.png

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.

IOC6_tunnel-ssh.png

  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:
    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).
  1. 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.
    .
    • Vous ouvrez deux terminaux sur votre machine locale. Sur le premier terminal, vous taper la commande suivante:
      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).
      .
    • Sur l'autre terminal, que vous n'aviez pas encore utilisé, vous tapez:
      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.
      .
    • 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 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 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 ?
#!/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 ?
#!/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:

fake2server.png

  • 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'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.
  2. 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

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

#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

#!/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'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 <META>

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. 

#!/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

<html>
 <head><title>Peri Web Server</title></head>
 <frameset rows="100,*" frameborder=0>
  <frame src="img/peri.png">
  <frame src="cgi-bin/main.py">
 </frameset>
</html>

main.py

#!/usr/bin/env python

html="""
<head>
  <title>Peri Web Server</title>
</head>
<body>
LEDS:<br/>
<form method="POST" action="led.py">
  <input name="val" cols="20"></input>
  <input type="submit" value="Entrer">
</form>
</body>
"""

print html

led.py

#!/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="""
<head>
  <title>Peri Web Server</title>
  <META HTTP-EQUIV="Refresh" CONTENT="1; URL=/cgi-bin/main.py">
</head>
<body>
LEDS:<br/>
<form method="POST" action="led.py">
  <input name="val" cols="20"></input>
  <input type="submit" value="Entrer">
  set %s
</form>
</body>
""" % (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.
Last modified 4 years ago Last modified on Mar 29, 2020, 5:57:38 PM