Programmer des pages Web dynamiques en Python pour Lighttpd

Dans l'article « Créer un serveur Web avec Lighttpd sur Raspberry Pi », un serveur Web Lighttpd avait été installé sur un Raspberry Pi. Tel que configuré alors, il ne permettait que de servir des pages Web statiques. Mais ce logiciel permet aussi de servir des pages Web CGI créées dynamiquement par programmation avec des langages informatiques comme C, C++, FastCGI, PHP, et même en Python.
Dans cet article, nous verrons comment configurer Lighttpd pour qu'il soit en mesure de servir des pages Web programmées en Python. Un exemple de script CGI en Python sera décrit pour servir de base à des scripts plus sophistiqués permettant de construire des pages dynamiques à partir de données collectées dans des bases de données comme MariaDB (une alternative en logiciel libre à MySQL).

Installation module CGI de Lighttpd

Lorsqu'on fait la liste des modules disponibles pour Lighttpd, on constate l'existence d'un module CGI configurable par le fichier 10-cgi.conf.
pi@raspi01:~ $ ls -l /etc/lighttpd/conf-available
total 104
-rw-r--r-- 1 root root 839 janv. 28  2019 05-auth.conf
-rw-r--r-- 1 root root  91 janv. 28  2019 10-accesslog.conf
-rw-r--r-- 1 root root 396 janv. 28  2019 10-cgi.conf
-rw-r--r-- 1 root root  63 janv. 28  2019 10-dir-listing.conf
-rw-r--r-- 1 root root  36 janv. 28  2019 10-evasive.conf
-rw-r--r-- 1 root root 128 janv. 28  2019 10-evhost.conf
-rw-r--r-- 1 root root 104 janv. 28  2019 10-expire.conf
-rw-r--r-- 1 root root 177 janv. 28  2019 10-fastcgi.conf
-rw-r--r-- 1 root root  42 janv. 28  2019 10-flv-streaming.conf
-rw-r--r-- 1 root root  82 janv. 28  2019 10-no-www.conf
-rw-r--r-- 1 root root 849 janv. 28  2019 10-proxy.conf
-rw-r--r-- 1 root root 176 janv. 28  2019 10-rewrite.conf
-rw-r--r-- 1 root root 253 janv. 28  2019 10-rrdtool.conf
-rw-r--r-- 1 root root 398 janv. 28  2019 10-simple-vhost.conf
-rw-r--r-- 1 root root 449 janv. 28  2019 10-sockproxy.conf
-rw-r--r-- 1 root root  99 janv. 28  2019 10-ssi.conf
-rw-r--r-- 1 root root 203 févr. 23  2019 10-ssl.conf
-rw-r--r-- 1 root root 460 janv. 28  2019 10-status.conf
-rw-r--r-- 1 root root 450 janv. 28  2019 10-userdir.conf
-rw-r--r-- 1 root root  38 janv. 28  2019 10-usertrack.conf
-rw-r--r-- 1 root root 168 janv. 28  2019 11-extforward.conf
-rw-r--r-- 1 root root 575 janv. 28  2019 15-fastcgi-php.conf
-rw-r--r-- 1 root root 508 janv. 28  2019 90-debian-doc.conf
-rw-r--r-- 1 root root  56 juil. 28  2013 90-javascript-alias.conf
-rw-r--r-- 1 root root 162 janv. 28  2019 99-unconfigured.conf
-rw-r--r-- 1 root root 843 janv. 28  2019 README
pi@raspi01:~ $ 

Activation du module CGI

Par défaut, le module CGI n'est pas activé pour Lighttpd. Le fichier README contient les indications pour activer ou désactiver les modules. L'activation d'un module se fait par l'invocation de la commande /usr/sbin/lighty-enable-mod.
pi@raspi01:~ $ sudo lighty-enable-mod cgi
Enabling cgi: ok
Run "service lighttpd force-reload" to enable changes
pi@raspi01:~ $ ls -l /etc/lighttpd/conf-enabled
total 0
lrwxrwxrwx 1 root root 29 mai   11 14:00 10-cgi.conf -> ../conf-available/10-cgi.conf
lrwxrwxrwx 1 root root 42 févr. 13 17:02 90-javascript-alias.conf -> ../conf-available/90-javascript-alias.conf
pi@raspi01:~ $
Cette commande crée un alias du fichier 10-cgi.conf dans le dossier /etc/lighttpd/conf-enabled. Tous les fichiers de configuration contenus dans ce dossier sont pris en compte dans le fichier de configuration principale /etc/lighttpd/lighttpd.conf par une directive include.
pi@raspi01:~ $ cat /etc/lighttpd/lighttpd.conf
server.modules = (
        "mod_indexfile",
        "mod_access",
        "mod_alias",
        "mod_redirect",
)

server.document-root        = "/var/www/html"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 80

# strict parsing and normalization of URL for consistency and security
# https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_http-parseoptsDetails
# (might need to explicitly set "url-path-2f-decode" = "disable"
#  if a specific application is encoding URLs inside url-path)
server.http-parseopts = (
  "header-strict"           => "enable",# default
  "host-strict"             => "enable",# default
  "host-normalize"          => "enable",# default
  "url-normalize-unreserved"=> "enable",# recommended highly
  "url-normalize-required"  => "enable",# recommended
  "url-ctrls-reject"        => "enable",# recommended
  "url-path-2f-decode"      => "enable",# recommended highly (unless breaks app)
 #"url-path-2f-reject"      => "enable",
  "url-path-dotseg-remove"  => "enable",# recommended highly (unless breaks app)
 #"url-path-dotseg-reject"  => "enable",
 #"url-query-20-plus"       => "enable",# consistency in query string
)

index-file.names            = ( "index.php", "index.html" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

compress.cache-dir          = "/var/cache/lighttpd/compress/"
compress.filetype           = ( "application/javascript", "text/css", "text/html", "text/plain" )

# default listening port for IPv6 falls back to the IPv4 port
include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
include_shell "/usr/share/lighttpd/create-mime.conf.pl"
include "/etc/lighttpd/conf-enabled/*.conf"

#server.compat-module-load   = "disable"
server.modules += (
        "mod_compress",
        "mod_dirlisting",
        "mod_staticfile",
)
pi@raspi01:~ $

Configuration du module CGI et de Lighttpd

Il est déconseillé de modifier le contenu du fichier lighttpd.conf. La configuration du module CGI doit s'effectuer dans le fichier 10-cgi.conf. Cependant, comme l'objectif est de rédiger des scripts en langage Python, il est nécessaire de modifier quelques paramètres dans ce fichier. En effet, le fichier lighttpd.conf est pré-configuré pour les langages PHP et PERL, mais pas pour le langage Python.
Les modifications à effectuer dans le fichier lighttpd.conf sont les suivantes :

server.modules = (
        "mod_indexfile",
        "mod_access",
        "mod_alias",
        "mod_redirect",
)

server.document-root        = "/var/www/html"
server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
server.errorlog             = "/var/log/lighttpd/error.log"
server.pid-file             = "/var/run/lighttpd.pid"
server.username             = "www-data"
server.groupname            = "www-data"
server.port                 = 80

# strict parsing and normalization of URL for consistency and security
# https://redmine.lighttpd.net/projects/lighttpd/wiki/Server_http-parseoptsDetails
# (might need to explicitly set "url-path-2f-decode" = "disable"
#  if a specific application is encoding URLs inside url-path)
server.http-parseopts = (
  "header-strict"           => "enable",# default
  "host-strict"             => "enable",# default
  "host-normalize"          => "enable",# default
  "url-normalize-unreserved"=> "enable",# recommended highly
  "url-normalize-required"  => "enable",# recommended
  "url-ctrls-reject"        => "enable",# recommended
  "url-path-2f-decode"      => "enable",# recommended highly (unless breaks app)
 #"url-path-2f-reject"      => "enable",
  "url-path-dotseg-remove"  => "enable",# recommended highly (unless breaks app)
 #"url-path-dotseg-reject"  => "enable",
 #"url-query-20-plus"       => "enable",# consistency in query string
)

index-file.names            = ( "index.py", "index.php", "index.html" )
url.access-deny             = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi", ".py" )

compress.cache-dir          = "/var/cache/lighttpd/compress/"
compress.filetype           = ( "application/javascript", "text/css", "text/html", "text/plain" )

# default listening port for IPv6 falls back to the IPv4 port
include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
include_shell "/usr/share/lighttpd/create-mime.conf.pl"
include "/etc/lighttpd/conf-enabled/*.conf"

#server.compat-module-load   = "disable"
server.modules += (
        "mod_compress",
        "mod_dirlisting",
        "mod_staticfile",
)
  • Le premier paramètre à modifier est index-file-names
    Ce paramètre définit quel est le nom du fichier de la page d'accueil du serveur Web lorsqu'on omet de préciser le nom de la ressource désirée dans la requête HTTP. L'ordre est important. Par exemple, tel que configuré ici, Lighttpd va d'abord chercher une page index.py et l'affiche s'il la trouve. Sinon il va chercher une page index.php qu'il affiche s'il la trouve. Sinon il va chercher une page index.html à défaut de quoi une erreur HTTP 403 est générée.
    Ici, on accorde la priorité au script index.py qu'il faut ajouter, car il ne figure pas par défaut dans la liste des pages d'accueil possibles. 
  • Le deuxième paramètre à modifier est static-file.exclude-extensions.
    Ce paramètre définit quelles sont les ressources qu'il ne faut pas renvoyer en plain texte. En effet, lorsque par exemple une ressource a l'extension .html, ou .js (JavaScript), ou encore .css (feuille de style CCS) ou .img (image), ces fichiers sont servis tels quels au navigateur client qui en a fait la requête. Et c'est le navigateur que se charge de leur interprétation, voire de leur exécution pour un script JavaScript sur le poste client. En revanche, les scripts PHP, PERL ou Python sont exécutés sur le serveur et c'est le résultat de l'exécution qui est transmis au navigateur.
    Ici, il faut rajouter l'extension .py pour que le code Python contenu dans le script soit exécuté par l'interpréteur Python installé sur le serveur, et non transmis tel quel au navigateur client qui, ne sachant pas comment l'interpréter, affichera le code du script sur le poste client.
La configuration du module CGI proprement dit se fait dans le fichier 10-cgi.conf. La modification peut s'effectuer indifféremment dans les répertoire /etc/lighttpd/conf-available ou /etc/lighttpd/conf-enabled car les deux fichiers 10-cgi.conf contenus respectivement dans ces deux répertoires sont l'alias l'un de l'autre.
# /usr/share/doc/lighttpd/cgi.txt

#1#
server.modules += ( "mod_cgi" )

#2#
#$HTTP["url"] =~ "^/cgi-bin/" {
#       alias.url += ( "/cgi-bin/" => "/usr/lib/cgi-bin/" )
#}

#3#
$HTTP["url"] =~ "^/" {
        cgi.assign = ( ".py" => "/usr/bin/python3" )
        alias.url += ( "/" => "/var/www/html/" )
}

#4#
#$HTTP["url"] =~ "^/" {
#       cgi.assign = ( ".py" => "/usr/bin/python3", )
#       alias.url += ( "/" => "/var/www/cgi-bin" )
#}

## Warning this represents a security risk, as it allow to execute any file
## with a .pl/.py even outside of /usr/lib/cgi-bin.
#
#cgi.assign      = (
#       ".pl"  => "/usr/bin/perl",
#       ".py"  => "/usr/bin/python",
#)

  1. La premier chose paramétrée est d'ajouter le module Lighttpd mod_cgi aux modules activés. Cette ligne est pré-configurée dans le fichier 10-cgi.conf.
  2. Le paramétrage suivant, associant l'url à la localisation physique des scripts, est plus difficile à comprendre.
    La configuration par défaut, surlignée en rose et mise en commentaire, paramètre le module pour que les scripts CGI soit rangé dans le répertoire /usr/bin/cgi-bin. Si un script /usr/bin/cgi-bin/moncgi.py est créé, l'url pour l'atteindre sera http://192.168.1.102/cgi-bin/moncgi.py (en supposant que l'adresse IP du Raspberry Pi soit 192.168.1.102). Pour des raisons de sécurité, il est recommandé de faire fonctionner les scripts CGI dans un répertoire différent que les pages statiques. En effet, dans un script CGI on peut faire à peu près tout ce que permet le langage de programmation utilisé.
    Il faut le comprendre comme suit :
    • La première ligne signifie que l'url des scripts est calculée à  partir de /cgi-bin/.  
    • L'url /cgi-bin/ correspond au répertoire physique /usr/lib/cgi-bin.
  3. Dans la plupart des cas, on assume de laisser fonctionner les scripts CGI dans le même répertoire que les pages statiques /var/www/html. Car de toute façon permettre l'exécution de script par une requête HTTP comporte toujours un risque.
    • La première ligne indique signifie  que l'url est calculée à partir de la racine du site.
    • La deuxième ligne signifie que lorsqu'une ressource a l'extension .py, c'est l'interpréteur /usr/bin/python3 qui exécute le script contenu dans la ressource. Il est possible d'ajouter autant de lignes nécessaires entre les accolades pour indiquer pour des langages différents ou pour des versions différentes d'un même langage quel est l'interpréteur qui doit être invoqué. Ainsi, on pourrait ajouter que pour l'extension .py2 c'est la version 2 de l'interpréteur Python /usr/bin/python2.
    • La troisième ligne signifie que, pour les scripts CGI, la racine du site Web correspond à /var/www/html. Autrement dit, si le script moncgi.py se trouve dans le répertoire /var/www/html, l'url de la requêtre doit être http://192.168.1.102/moncgi.py
  4. La configuration surlignée en bleu ciel en commentaire est un bon compromis, car il permet de stocker les scripts CGI exécutables dans un répertoire différent que les pages Web statiques tout en laissant l'impression à  l'internaute qu'il se trouvent ans le même répertoire. Si moncgi.py se trouve dans le répertoire /var/www/cgi-bin et qu'une autre ressource mapage.html se trouvent dans le répertoire /var/www/html, les urls respectives de ces deux ressources sont http://192.168.1.102/moncgi.py et http://192.168.1.102/mapage.html.

Mon premier script CGI en Python

Les scripts CGI permettent de générer des pages Web par programmation. Le contenu de certains éléments HTML peut être extrait de données locales située n'importe où sur le serveur comme des fichiers ou de base de données. Mais ils permettent aussi de récupérer des données saisies par l'utilisateur dans un formulaire Web pour déclencher des actions sur le serveurs.
En Python, il est possible à peu près tous ce que permet le langage. Mais pour générer une page Web, quelques précautions doivent être prises afin que cela fonctionne dans le navigateur client. Le script présenté ici n'a rien d’extraordinaire puisqu'il se contente de générer une page Web qui pourrait tout aussi bien être créée en statique directement au format HTML. Mais il a l'avantage de mettre en évidence quelques astuces pour que la page Web s'affiche à coup sûr.
#!/usr/bin/env python3
# coding: utf-8
#1#

#2#
import cgi

#3#
print("Content-type: text/html; charset=utf-8\n")

#4#
print("""
<!DOCTYPE html>
<head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Page d'accueil Python</title>
</head>
<body>
        <h1>Page d'accueil Python</h1>
        <p>Cette page est générée par un programme Python</p>
</body>
</html>
""")
  1. La première ligne indique que le code Python contenu dans le script a été rédigé pour Python3. Ceci est pour être cohérent avec la configuration module CGI de Lighttpd.
    La seconde ligne indique que le jeu de caractères utilisé est UTF-8. Ce qui permet de ne pas avoir à gérer des entités HTML et des caractères d’échappement pour les caractères accentués.
  2. La première instruction Python  est d'importer le module Python cgi.
  3. La seconde instruction Python est obligatoire pour indiquer que c'est du code HTML qui va être envoyé en réponse à la requête HTTP. Il est prudent aussi de préciser que le jeu de caractères du code HTML est UTF-8 pour que les caractères accentués soient interprétés correctement par le navigateur client.
    Il faut insister sur le retour de chariot symbolisé par le caractère d'échappement \n qui termine l'instruction print. En effet, il faut ABSOLUMENT une ligne vide entre le flux de texte envoyé vers le client par cette instruction et le flux de texte HTML qui va suivre pour constituer le contenu de la page Web proprement dit.
  4. Le reste est un flux de texte contenant du code HTML. Le code HTML peut être fractionné en plusieurs appel de l'instruction print.
    L'usage des triples guillemets permet de passer à la ligne en observant la même structure que le code HTML. De plus, son usage en intercalant du code HTML entre print(""" et """) ressemble à la syntaxe qu'on utilise avec des langages comme PHP entre <?php et ?>.  A la différence que le code PHP est à l'intérieur de <?php et ?>, alors que c'est le code HTML qui se trouve entre print(""" et """) en Python. 

Test de l'installation

Si le code ci-dessus est mis dans un fichier index.py situé dans le répertoire /var/www/html, le fait de solliciter la requête http://192.168.1.102 exécute le script Python pour afficher la page Web suivante :

Conclusion

Après avoir appris à exposer des pages Web statiques grâce au serveur Lighttpd, il est maintenant possible de créer dynamiquement du contenu grâce à la technologie CGI en utilisant le langage Python. Certes, la page Web générée dans cet exemple est rudimentaire. Mais dans un futur article, on utilisera ce concept pour saisir des données dans un formulaire Web pour alimenter une base de données MariaDB, puis à afficher du contenu grâce à des requêtes SQL sur cette même base.

Commentaires

Posts les plus consultés de ce blog

Gérer la mise en veille

Configurer VSCode pour programmer et déboguer à distance sur Raspberry Pi

Créer un nouvel utilisateur Raspbian