Un cours pour débuter en Perl


previous up contents next

Sous-sections


18. L'écriture de scripts CGI

18.1 Introduction

Common Gateway Interface (CGI18.1) est une spécification d'interface permettant d'exécuter des procédures externes depuis un service WWW. CGI spécifie (du côté du serveur) comment doit être initialisée la procédure externe et comment elle doit accéder aux informations à traiter. Les informations transmises à la procédure externe sont :
  • des variables qui correspondent aux en-têtes de la requête HTTP qui a déclenché l'appel de la procédure externe (HTTP_REFERER, HTTP_USER_AGENT,
    HTTP_CONNECTION, HTTP_HOST, ...);
  • des variables relatives au corps d'un document transmis ou propres au contexte CGI (REQUEST_METHOD, CONTENT_TYPE, CONTENT_LENGTH, REMOTE_HOST, QUERY_STRING, ...);
  • un corps de document éventuel.

CGI indique également que si un corps de document est transmis, il doit être accédé via l'entrée standard de la procédure externe. De plus la procédure externe (le script CGI) doit toujours rendre un résultat via sa sortie standard. La spécification des ces deux mécanismes impose (au serveur HTTP) un mode de création assez lourd et couteux du script CGI (fork + redirection de l'entrée et de la sortie standard + exec). C'est la raison principale du manque de performance des scripts CGI, à chaque requête, le système d'exploitation est fortement sollicité.

Dans la pratique, malgré son manque de performance, CGI reste très utilisé pour transmettre des informations d'un client WWW vers un service particulier (via un serveur HTTP). C'est notamment ainsi que fonctionnent nombre de formulaires WWW, le synoptique des échanges entre client, serveur et script CGI est alors le suivant :

  • un client WWW affiche un formulaire ;
  • l'utilisateur complète les champs à transmettre et valide le contenu qui est transmis. Si la méthode HTTP utilisée pour transmettre le formulaire est GET, alors les champs (l'information à transmettre) sont transmis dans l'URL d'appel du script CGI et lui sont passés via la variable QUERY_STRING.
    Si la méhode HTTP est POST, alors les champs sont transmis dans un corps de document (dont le CONTENT_TYPE est application/x-www-form-urlencoded) et sont passés au script CGI via son entrée standard. Cette dernière façon de procéder est fortement conseillée.
    Le type de document application/x-www-form-urlencoded implique que les champs du formulaire sont séparés entre eux par le caractère &, qu'ils sont précédés de leur nom (attribut NAME de la balise HTML $<$FORM$>$) et du signe =, que les caractères accentués et les espaces sont encodés comme spécifié dans le RFC1738 sur les URL;

  • un serveur HTTP réceptionne la requête et initialise le script CGI référencé par le formulaire WWW ;
  • le script CGI effectue le traitement demandé et se charge d'émettre un résultat à destination du client WWW instigateur de la requête.

En dehors de toute aide extérieure, un programmeur de script CGI doit donc décoder le application/x-www-form-urlencoded pour accéder individuellement aux champs transmis, il doit également prendre en charge l'émission du résultat (et donc générer des en-têtes HTTP correctes suivies d'un corps de document HTML). En Perl diverses contributions permettent de faciliter ces deux tâches, les plus connues sont cgi-lib.pl et CGI.pm.

18.2 Un exemple

Dans l'exemple suivant on désire compléter un formulaire WWW comportant deux champs (nom et prénom), le transmettre à une procédure CGI qui pourrait stocker les champs transmis dans un fichier ... mais se contentera ici d'avertir l'utilisateur de la prise en compte de l'information. Le code HTML du formulaire est placé dans un document à part (extérieur à la procédure CGI18.2), on vérifie que la méthode HTTP utilisée est bien POST et on montre à l'utilisateur les renseignements obtenus sur lui (machine, client WWW, ...).

  

<HTML>
<!-- Source HTML permettant d'afficher le formulaire et
     d'appeler la procédure CGI référencée par l'attribut
     action de la balise <FORM>.
-->
<HEAD>
<TITLE> Test CGI </TITLE>
</HEAD>
<BODY>
<H1> Test CGI </H1>
<HR>
<FORM action="http://madeo.irisa.fr/cgi-bin/tstcgi" 
      method="POST">
  Nom :   <INPUT TYPE="text" NAME="nom"    SIZE="25"> <BR>
  Prénom: <INPUT TYPE="text" NAME="prenom" SIZE="15"> <BR>
  <INPUT TYPE="submit" VALUE="OK">
  <INPUT TYPE="reset" VALUE="Annuler">
</FORM>
<HR>
</BODY>
</HTML>
  

18.2.1 Utiliser cgi-lib.pl

La première bibliothèque qui a été diffusée pour écrire des scripts CGI en Perl est cgi-lib.pl. Elle reste aujourd'hui très utilisée et présente les caractéristiques suivantes :

  • permet de récupérer les champs transmis dans un tableau associatif ;
  • gère le transfert de fichiers dans le sens client serveur (upload) ;
  • globalement très simple à utiliser.

Le script Perl suivant propose d'utiliser cgi-lib.pl pour réaliser notre exemple, il utilise les fonctions suivantes :

  • Methpost() qui rend vrai si la méthode HTTP utilisée est POST;
  • ReadParse() qui prend une référence sur un hash et le complète par une association entre le nom des champs (tel que spécifiés par l'attribut NAME de la balise $<$FORM$>$) et leurs valeurs respectives;
  • PrintHeader() qui indique au navigateur qu'il va recevoir un document HTML. Cette fonction prend aussi à sa charge l'émission d'une ligne vide pour séparer les en-têtes du corps de document (cf. RFC2068 sur HTTP/1.1);
  • CgiDie pour quitter la procédure en émettant un message d'avertissement à l'utilisateur;
  • ...
  

#!c:\perl\bin\perl.exe
#  TestCGI utilise cgi-lib pour acquerir les champs du 
#  formulaire et generer le HTML resultat. On verifie 
#  que la methode est POST, que les champs ne sont pas 
#  vides et on informe l'utilisateur du resultat.
require "cgi-lib.pl";
$champs= {};   # $champs est une reference sur un hash vide
if (MethPost()) {
  ReadParse($champs);
  if ($champs->{"nom"} eq "" || $champs->{"prenom"} eq "")
 CgiDie("Il faut remplir les champs !");
  }    
  print (PrintHeader(), 
         HtmlTop("Resultat de votre requete"),
         "Nom : ", $champs->{"nom"}, "<BR>",
         "Prenom :", $champs->{"prenom"}, "<BR>", 
         "vous utilisez $ENV{HTTP_USER_AGENT}",
         "depuis la machine $ENV{REMOTE_ADDR}",
         HtmlBot());
}
else {
    CgiDie("Hum ... Que faites vous !");
}
  


18.2.2 Utiliser CGI.pm

CGI.pm est un autre outil facilitant la réalisation de script CGI en Perl. CGI.pm peut être utilisé de deux façons :
  • en important des fonctions dans l'espace du script, CGI.pm est alors utilisé comme un package;
  • en utilisant un objet de classe CGI comme dans la réalisation (ci-après) de notre exemple.

  

#!c:\perl\bin\perl.exe
#  TestCGI utilise CGI.pm pour acquerir les champs du 
#  formulaire et generer le HTML resultat. On verifie 
#  que la methode est POST, que les champs ne sont pas 
#  vides et on informe l'utilisateur du resultat.
use CGI;
$req = new CGI;
print $req->header();
print $req->start_html('Résultat de la requête');
if ($req->request_method() eq "POST") {
   if ($req->param('nom')    eq " " || 
       $req->param('prenom') eq " " ) {
   print $req->h1('Il faut remplir les champs');
   }
   else {
     print ("Nom : ", $req->param('nom'),
            $req->br,
            "Prenom : ", $req->param('prenom'),
            $req->br,
            " vous utilisez", $req->user_agent(),
            "depuis la machine ",$req->remote_addr());
   }
}
else {
print $req->h1('Hum ... Que faites vous !');
}
print $req->end_html;
  

CGI.pm peut semble run peu plus lourd à utiliser que cgi-lib.pl, il est toutefois plus élaboré pour :

  • générer les en-têtes HTTP, dans notre exemple, $req-$>$header se contente de positionner Content-Type: text/html (suivi d'une ligne vide), mais pourrait aussi indiquer une en-tête expire ($req-$>$header(-expires=$>$'now');) pour inhiber les caches;
  • générer du code HTML (notamment des tables, des formulaires, ...);
  • être utilisé dans un contexte FastCGI (cf. 18.4) ;
  • mettre au point les scripts sans passer par un navigateur WWW et un serveur HTTP. CGI.pm reconnait s'il est appelé dans un contexte dit offline et invite alors l'utilisateur à rentrer à la main des couples clef=valeur pour simuler la chaîne complète d'un environnement CGI.

18.3 Envoyer un fichier (upload)

Il est parfois intéressant de transférer des documents complets depuis un client WWW vers un serveur HTTP. CGI.pm ainsi que cgi-lib.pl offrent cette possibilité. Elle s'appuie sur le transfert dans le corps d'un document (comprenant plusieurs parties), des champs éventuels d'un formulaire suivi des différents fichiers.

On peut compléter notre exemple en demandant à l'utilisateur de fournir deux fichiers qui seront transférés sur le serveur et transmis à notre script (via cgi-lib.pl ici). Le code HTML peut être modifié de la façon suivante :

  

<HTML>
<!-- Le  document est composé de plusieurs parties, donc
 (ENCTYPE="multipart/form-data") + ajout de 2 champs "file".
-->
<HEAD> <TITLE> Test CGI </TITLE> </HEAD>
<BODY>
<H1> Test CGI </H1><HR>
<FORM action="http://madeo.irisa.fr/cgi-bin/tfic.pl" 
      ENCTYPE="multipart/form-data" method="POST">
  Nom :   <INPUT TYPE="text" NAME="nom"    SIZE="25"> <BR>
  Prénom: <INPUT TYPE="text" NAME="prenom" SIZE="15"> <BR>
  <INPUT TYPE="file" NAME="fic1" SIZE=50> <BR>
  <INPUT TYPE="file" NAME="fic2" SIZE=50> <BR>
  <INPUT TYPE="submit" VALUE="OK">
  <INPUT TYPE="reset" VALUE="Annuler">
</FORM> </BODY> </HTML>
  

Dans le cas d'un type de document multipart/form-data les arguments de la fonction ReadParse sont 4 références à des tableaux associatifs correspondants aux champs du formulaire, aux nom des fichiers transmis, aux Content-Type des fichiers transmis et aux noms des fichiers reçus. Notre procédure peut donc être modifiée de la façon suivante :

  

#!c:\perl\bin\perl.exe
#
#  Réception des deux fichiers.
require "cgi-lib.pl";
$champs = {};       # $champs, $client_fn, 
$client_fn = {};    # $client_ct, $serveur_fn
$client_ct = {};    # sont des références sur 
$serveur_fn = {};   # des hashes vides.
$cgi_lib::writefiles = "c:\tmp";  # Répertoire dans lequel
                                  # les fichiers transmis 
                                  # seront déposés.
if (MethPost()) {
  ReadParse ($champs, $client_fn, $client_ct, $serveur_fn);
  if ($champs->{"nom"} eq "" || $champs->{"prenom"} eq "") {
	CgiDie("Il faut remplir les champs !");
  }    
  print (PrintHeader(), 
         HtnlTop("Resultat de votre requete"),
         "Nom : ", $champs->{"nom"}, "<BR>",
         "Prenom :", $champs->{"prenom"}, "<BR>",
         $serveur_fn->{'fic1'}, # nom du fichier 1 (serveur)
         $serveur_fn->{'fic2'}, # nom du fichier 2 (serveur)
         $client_fn->{'fic1'},  # nom du fichier 1 (client)
         $client_fn->{'fic2'},  # nom du fichier 2 (client)
         $client_ct->{'fic1'},  # Content-Type fichier 1
         $client_ct->{'fic2'},  # Content-Type fichier 2
         HtmlBot());
# Une procedure opérationnelle pourrait maintenant ouvrir
# les fichiers correspondants et effectuer un traitement.
}
else {
    CgiDie("Hum ... Que faites vous !");
}
  

Pour qu'une procédure effectuant du transfert de fichiers se déroule normalement, il convient que le serveur HTTP soit autorisé à écrire dans le répertoire $cgi_lib::writefiles, il faut également que celui-ci soit correctement dimensionné.


18.4 Utiliser FastCGI

18.4.1 Introduction

Pour contourner le manque de performance des scripts CGI écrits en Perl, deux solutions sont envisageables :
  • utiliser mod_perl18.3 qui lie APACHE18.4 et Perl et permet d'éviter la création de processus à chaque appel de procédure. Cette solution présente l'avantage de permettre l'écriture en Perl de modules pour APACHE18.5, mais elle introduit quelques inconvénients, notamment une très forte augmentation de la taille mémoire utilisée par un processus httpd;
  • utiliser FastCGI18.6 qui fait communiquer le serveur HTTP et la procédure externe via une socket. La procédure externe fonctionne alors comme un serveur à l'écoute sur un port, elle traite les requêtes de son client (le serveur HTTP) qui lui transmet les données émises par les navigateurs WWW. La procédure externe est démarrée à l'initialisation du serveur HTTP, elle est persistante, le gain de performance par rapport à CGI est très significatif.
    FastCGI permet également de délocaliser la procédure externe (la faire se dérouler sur une machine distincte de celle qui héberge le serveur HTTP), c'est une fonctionnalité très intéressante, elle permet de répartir les traitements.
CGI.pm permet d'utiliser FastCGI comme canal de communication, en conséquence les changements à apporter pour faire migrer une procédure de CGI vers FastCGI sont très minimes (cf. exemple ci-dessous).

18.4.2 Un exemple

On reprend ici l'exemple de la section 18.2.2, les changements à effectuer ont pour but d'indiquer à CGI.pm de basculer dans un contexte FastCGI et de rendre la procédure persistante (boucler sur l'attente de l'arrivée de données à traiter). Pour mettre en évidence l'effet de la persistance de la procédure, on ajoute un compteur de requêtes.

  

#!/usr/local/bin/perl
#  TestCGI utilise CGI.pm pour acquerir les champs du 
#  formulaire et generer le HTML resultat. On verifie 
#  que la methode est POST, que les champs ne sont pas 
#  vides et on informe l'utilisateur du resultat.
use CGI::Fast;
$Cptr = 0;
while ($req=new CGI::Fast) {
$Cptr++;
print $req->header();
print $req->start_html('Résultat de la requête');
if ($req->request_method() eq "POST") {
   if ($req->param('nom')    eq " " || 
       $req->param('prenom') eq " " ) {
   print $req->h1('Il faut remplir les champs');
   }
   else {
     print ("Requête numéro : ", $Cptr, $req->br);
     print ("Nom : ", $req->param('nom'),
            $req->br,
            "Prenom : ", $req->param('prenom'),
            $req->br,
            " vous utilisez", $req->user_agent(),
            "depuis la machine ",$req->remote_addr());
   }
}
else {
print $req->h1('Hum ... Que faites vous !');
}
print $req->end_html;
}
  


previous up contents next

FD