I. Introduction

I-A. Précisions

  • Pour faire du CGI en Delphi il faut un serveur Web sous Windows évidemment.
  • Mon expérience en la matière est pour un Serveur Lotus Domino sous NT.
  • J'utilise Delphi 2.0, mais ce tutoriel est compatible Delphi 3,4 et 5.

Remarque : si vous désirez utiliser des DLL ISAP/NSAPI, utilisez Delphi 5Delphi 5 ; ce tutoriel reste cependant utile si vous désirez comprendre comment fonctionnent les CGI.

I-B. Le principe

Dans une page HTML (ou directement l'URL dans le navigateur) tu mets un lien vers ton programme. Voici quelques exemples :

  • appel par un lien : <a href="/cgi-bin/program.exe"> ;
  • demander une image : <img src="/cgi-bin/program.exe"> ;
  • formulaire en GET : <form method=GET action="/cgi-bin/program.exe">…</form> ;
  • formulaire en POST : <form method=POST action="/cgi-bin/program.exe">…</form> ;
  • appel direct : http://www.tonserver.fr/cgi-bin/program.exe.

I-C. cgi-bin

C'est un alias déclaré sur le serveur qui pointe sur le répertoire qui contient les programmes CGI (c:\internet\delphi\cgi) par exemple…

I-D. Lancement du programme

Lorsque tu cliques sur le lien (ou quand l'image se charge…) le serveur exécute le programme (et pas le poste de travail) et attend la réponse.

I-E. La réponse du programme

Le plus simple est de faire une application « console » {$apptype console} qui ressemble à une application DOS mais sous Windows 95/NT. Ce n'est pas obligatoire mais ça permet de tester en locale à l'écran…

 
Sélectionnez
Program ExempleCGI;

{$apptype console}

begin
    WriteLn('content-type: text/html');
    Writeln;
    WriteLn('Bonjour le monde !');
end.

Voici le programme CGI le plus simple !

Content-type : c'est la description du contenu (ici du texte HTML).

Ligne vierge : c'est OBLIGATOIRE, c'est pour dire « fin de l'entête/début du document » (le navigateur n'affiche pas l'entête).

Le document : c'est ce que tu affiches dans « Affichage/Source ».

I-F. WriteLn

Eh oui ! Il suffit d'envoyer le résultat à l'écran, en fait le serveur récupère ce que tu envoies en « sortie standard » pour l'envoyer au navigateur.

D'ailleurs, on peut s'amuser a faire du CGI avec des .BAT !!!

 
Sélectionnez
@ECHO OFF
ECHO content-type: text/html
ECHO.
ECHO ^<HTML^>^<HEAD^>^<TITLE^>^</TITLE^>^</HEAD^>^<BODY>
ECHO Bonjour le monde !
ECHO ^</BODY^>^</HTML^>

Notez l'utilisation du '^' devant les symboles réservés du DOS (<, >, &, etc.).

Bon c'est pas mal comme introduction je pense…

II. Les paramètres

II-A. GET et POST

Il existe (au moins ?) deux méthodes pour passer des paramètres à un programme CGI :

  • <form method=GET action="program.exe"> ;
  • <form method=POST action="program.exe">.

Pour déterminer la méthode employée, il suffit dans votre programme Delphi de regarder la variable d'environnement REQUEST_METHOD. J'en profile pour vous présenter la fonction que j'utilise pour les lire les variables d'environnement :

 
Sélectionnez
function getvar(varname:string):string;
    var
        buffer:array[0..1024] of char;
        size:integer;
    begin
        size:=GetEnvironmentVariable(PChar(varname),buffer,sizeof(buffer));
        if size=0 then getvar:='' else getvar:=String(buffer);
end; 

Mais c'est juste parce que je préfère travailler avec des string…

Sous DOS (en .BAT) ça donne :

 
Sélectionnez
@ECHO OFF
ECHO content-type: text/html
ECHO.
ECHO ^<HTML^>^<HEAD^>^<TITLE^>^</TITLE^>^</HEAD^>^<BODY^> ECHO REQUEST_METHOD=%REQUEST_METHOD%
ECHO ^</BODY^>^</HTML^>

Notez l'utilisation du '^' devant les symboles réservés du DOS (<, >, &, etc.).

Donc, on commence par faire un GetVar('REQUEST_METHOD') qui renvoie 'GET' ou 'POST'.

Selon la norme CGIla norme CGI, les paramètres sont alors lus :

  • dans la variable QUERY_STRING pour la méthode GET ;
  • dans l'entrée standard (ReadLn) pour la méthode POST.

La méthode POST est en fait utilisée quand le nombre de paramètres est trop important, ce qui ne veut pas dire grand chose, mais qui peut se comprendre… N'oublions pas que la méthode GET utilise une variable d'environnement pour tous les paramètres…

II-B. QUERY_STRING

La variable QUERY_STRING contient donc la liste de paramètres sous une forme qui ne vous est sûrement pas inconnue… mais d'abord le code HTML :

 
Sélectionnez
<form method="GET" action="program.exe">
    <input type=text name="toto" value="titi">
    <input type=submit value="GO">
</form>

Ce qui donne :

En cliquant sur « GO » (pas ici, ça marchera pas !) le programme « program.exe » est appelé sous la forme : http://www.server.fr/cgi-bin/program.exe?toto=titi

Et c'est ce qui suit le '?' qu'on retrouvera dans QUERY_STRING… Notez que rien n'empêche d'indiquer les paramètres dans un lien « HREF= ».

Dernière précision, si la requête possède plusieurs paramètres ils sont séparés par des '&' ; de plus certains caractères sont codés en hexadécimal (comme le '&' par exemple) sous la forme « %HH », où « HH » est la valeur hexadécimale du code caractère. Ainsi '&' sera codé par « %26 ».

Je disais plus haut que vous aviez sans doute déjà vu ce genre de chose : sur yahoo.com, recherchez « cgi + français », l'URL apparaît alors sous la forme :

http://search.yahoo.com/bin/search?p=cgi+%2B+fran%E7ais

Vous appelez donc le programme « search » avec une variable « p » égale à « cgi + français » (où les espaces sont remplacés par des '+', le '+' par « %2B » et le 'ç' par « %E7 »).

II-C. La méthode POST

Personnellement, je n'ai pas réussi à faire fonctionner cette méthode…

Nouveau ! En fait les données de la méthode POST sont à lire dans l'entrée standard. Pour savoir la taille des données, lire la variable CONTENT_LENGTH.

 
Sélectionnez
// get parm string
if getvar('REQUEST_METHOD')='POST' then begin
    parmstring:=getvar('CONTENT_LENGTH');
    if parmstring<>'' then begin
        size:=strtoint(parmstring);
        setlength(parmstring,size);
        for i:=1 to size do read(parmstring[i]);
    end;
end else
    parmstring:=getvar('QUERY_STRING'); 

Je vous propose donc ce petit programme CGI, qui permet tout simplement d'afficher ce qui se passe sur le serveur :

Ce qui se traduit par :

 
Sélectionnez
program log;

{$apptype console}

uses
    windows, sysutils;

var
    i:integer;
    s:string;
    p:pchar;

    flog:textfile;

begin
    assignfile(flog,'c:\temp\log.txt');
    rewrite(flog);

    WriteLn('Content-Type: text/html');
    WriteLn('');

    WriteLn('<html><head><title>Dump CGI</title></head><body>');
    WriteLn('<h1>Dump CGI:</h1>');
    WriteLn('<a href=#Parms>Paramètres du programme</a><br>');
    WriteLn('<a href=#Query>Paramètres CGI</a><br>');
    WriteLn('<a href=#Env>Variables d''environnement</a><br>');
    WriteLn('<a href=#Info>Plus d''info</a><br>');
    WriteLn('<hr>');

    WriteLn('<a name=Parms><h2>ParamCount=',IntToStr(ParamCount),'</h2><ul>');
    WriteLn(fLog,'ParamCount=',IntToStr(ParamCount));

    for i:=0 to ParamCount do begin
        WriteLn('<li>',ParamStr(i));
        WriteLn(fLog,'-',ParamStr(i));
    end;

    // fichier en entrée
    WriteLn(fLog,'Input :');
    WriteLn('<h2>StdInput:</h2><ul>');
    if Not Eof(Input) then begin
        Read(Input,s);
        WriteLn('<li>',s);
        WriteLn(fLog,s);
    end;

    Writeln(fLog,'QUERY_STRING=',ParmString);

    WriteLn('<a name=Env><h2>Variables d''environnement :</h2><ul>');
    p:=GetEnvironmentStrings;
    while StrLen(p)<>0 do begin
        WriteLn('<li>',p);
        WriteLn(fLog,':',p);
        p:=strend(p);
        inc(p);
    end;
    WriteLn('</ul><hr>');

    WriteLn('<a name=Info><a href="http://www.multimania.com/tothpaul">');
    WriteLn('plus d''info sur le CGI</a>');
    WriteLn('</body></html>');

end.

Remarque : j'utilise un fichier log en parallèle pour les cas où la requête n'aboutit pas.

À +, pour la suite…

III. Redirection

III-A. Ce que l'on sait déjà

Nous savons que le programme CGI renvoie au serveur une entête non visible dans le navigateur :

 
Sélectionnez
WriteLn('Content-Type: text/html');
WriteLn('');

III-B. Ce que je n'avais pas dit

Eh bien il faut savoir qu'on peut faire des tas de choses avec cet entête, notamment, le CGI peut renvoyer sur une autre page… Il suffit pour cela de répondre :

 
Sélectionnez
WriteLn('Location: redirection.htm');

Il faut aussi savoir que votre serveur va ajouter des informations dans cet entête, pour vous en convaincre, vous pouvez utiliser mon « navigateur Web » qui a la particularité de ne pas traiter le HTML, et d'afficher l'entête HTTP.

Si vous demandez l'URL http://yahoo.com, voici ce que vous recevez :

 
Sélectionnez
HTTP/1.0 302 Found
Location: http://www.yahoo.com

Les navigateurs demandent alors l'URL http://www.yahoo.com pour recevoir :

 
Sélectionnez
HTTP/1.0 200 OK
Content-Length: 9332
Expires: Wed, 18 Mar 1998 08:00:03 GMT      
Content-Type: text/html

<html><head><title>Yahoo!</title><base href="http://www.yahoo.com/"></head>
<body><center><form action="http://search.yahoo.com/bin/search">
<a href="/bin/top3"><img width=460 height=59 border=0 usemap="#top3" ismap src="http://us.yimg.com/i/main32.gif" alt="Yahoo!">
</a><br><table cellpadding=3 cellspacing=0><tr><td align=center nowrap>
…

Il s'agit tout simplement d'une redirection !

Dernière remarque, c'est le serveur qui répond « HTTP/1.0… », qui calcule « Content-Length:… » vous n'avez pas à les renseigner.

C'est tout !

IV. Les images

IV-A. Ce que l'on sait déjà

Nous savons que le programme CGI renvoie au serveur une entête non visible dans le navigateur :

  delphi   0 1  
WriteLn('Content-Type: text/html');
WriteLn('');

IV-B. Ce que je n'avais pas dit

C'est qu'on peut très bien renvoyer un autre type de donnée ! Comme par exemple :

  delphi   0 1  
WriteLn('Content-Type: image/gif');
WriteLn('');

Il s'agit maintenant d'envoyer l'image…

IV-C. Envoyer des données binaires

Dans un premier temps, voici comment envoyer des données binaires sur StdOutput.

J'ai écrit une procédure générique pour envoyer un TStream vers StdOutput avec un paramètre Head pour pouvoir ajouter un entête :

 
Sélectionnez
procedure WriteStream(stream:TStream;Head:String);
var
    buffer:array[0..1024] of char;
    l:integer;
    f:file;
begin
    assignfile(f,''); rewrite(f,1); // this will overide any previous output (WriteLn)
                                   // but I can't find an other way to do this quickly !
    if head<>'' then BlockWrite(f,head[1],length(head));
    Stream.position:=0;
    l:=Stream.Read(buffer,sizeof(buffer));
    while l>0 do begin
        BlockWrite(f,buffer,l);
        l:=Stream.Read(buffer,sizeof(buffer));
    end;
    closefile(f);
end;

On utilisera également WriteFile pour envoyer un fichier existant :

 
Sélectionnez
procedure WriteFile(FileName:string;Head:string);
var
    s:TFileStream;
begin
    s:=TFileStream.Create(FileName,fmOpenRead);
    WriteStream(s,Head);
 end;

IV-D. Reste à créer un GIF

Pour cela, j'ai une unité GIF qui sauvegarde un TBitmap dans un Stream au format GIF. Il reste alors à utiliser :

 
Sélectionnez
procedure WriteBitmapAsGIF(Bitmap:TBitmap);
Var
    GifStream:TMemoryStream;
begin
    Try
        GifStream:=TMemoryStream.Create;
        BitmapToGifStream(Bitmap,GifStream);
        WriteStream(GifStream,'Content-type: image/gif'+#13#10+
                             #13#10);
    Finally
        GifStream.Free;
    end;
end;

Mais oui non ! Je vous la file pas l'unité GIF !

C'est tout !

V. Protection par mot de passe

V-A. Ce que l'on sait déjà

Lorsque le programme CGI renseigne l'entête CGI, le serveur la complète avec divers informations ; notamment il indique que la requête s'est bien déroulée :

 
Sélectionnez
HTTP/1.0 200 OK

V-B. Ce qu'il faut savoir

Si vous utilisez mon « navigateur web text » (voir ma page Delphi), vous verrez que l'entête HTTP d'un site sécurisé est un peu particulière :

 
Sélectionnez
HTTP/1.0 401 Unauthorized
Content-type: text/html
WWW-Authenticate: Basic realm="/MyRealm"

<html><head><title>401 Unauthorized</title></head><body>
<h1>You need a password to access this page!</h1>
</body></html>

Ici le serveur renvoie l'erreur « 401 Unauthorized », dès lors votre navigateur sait qu'il se trouve sur un site protégé. Sans afficher d'erreur il va vous demander le mot de passe pour le domaine précisé (realm="/MyRealm"). Si vous saisissez un profil/mot de passe valide pour le serveur, celui-ci vous donnera accès à la page désirée, sinon le navigateur affiche le message qui suit l'entête 401.

Vous n'avez pas tout compris, bon, relisez tranquillement toute l'info y est !

V-C. L'entête HTTP

Si vous avez compris ce qui précède, vous êtes déjà en train de saisir le programme suivant :

 
Sélectionnez
Program EssaiPWD;
{$apptype console}
begin
    WriteLn('HTTP/1.0 401 Unauthorized');
    WriteLn('Content-type: text/html');
    WriteLn('WWW-Authenticate: Basic realm="/Essai"');
    WriteLn;
    WriteLn('mot de passe s.v.p.');
end;

Et vous râlez déjà contre votre serveur (ou contre moi), car ça marche pas ! Pas de panique ! C'est normal, je vous avais déjà dit que le serveur complétait l'entête HTTP… Eh bien, le CGI ci-dessus arrive sur le navigateur de cette façon :

 
Sélectionnez
HTTP/1.0 200 OK
HTTP/1.0 401 Unauthorized
Content-type: text/html
WWW-Authenticate: Basic realm="/Essai"

mot de passe s.v.p.

La première ligne indique donc que la requête se passe bien (merci le serveur), la seconde est ignorée. J'ai pas mal galéré pour trouver comment éviter ça, finalement la réponse est tout simplement dans la norme CGI : le nom du CGI doit commencer par « nph- » s'il gère lui même l'entête HTTP.

Et voilà, tout est dit, il suffit de renommer le programme ci-dessus en « NPH-ESSAIPWD.EXE » et ça marche !

Enfin, ça marche presque puisque le mot de passe n'est pas validé !

V-D. WWW-Authenticate

Alors, le navigateur reçoit une demande d'authentification : WWW-Authenticate: Basic realm="/MyRealm".

En réponse, le CGI reçoit le profil/mot de passe dans la variable d'environnement HTTP_AUTHORIZATION :

 
Sélectionnez
HTTP_AUTHORIZATION=Basic dXNlcjpwYXNzd29yZA==

Pour valider cette information, il faut la décoder… Elle est en effet au format base64.

Petit rappel sur les bases (cours de mathématiques de primaire !) :

  • habituellement on travaille en base 10 : 0 … 9
  • en informatique on utilise couramment la base 16 : 0 … 9, 'A' … 'F' ;
  • sur internet on utilise la base 64 : 'A' … 'Z', 'a' … z', '+', '/'.

Vous trouverez dans le programme exemple une unité base64 qui prend en charge de codage/décodage d'un type String en base 64.

Le texte ci-dessus une fois décodé donne tout simplement : « user:password » .

Il ne reste plus au CGI qu'à accepter le mot de passe en envoyant les informations demandées ou renvoyer l'erreur 401.

Une autre alternative sur un mot de passe invalide étant de rediriger sur la page d'accueil par exemple.

Pour la mise en pratique, téléchargez le programme login (sans oublier de renommer l'exécutable et le point INI en nph-login.exe et npg-login.ini).

C'est tout !

VI. Les cookies

VI-A. Ce que l'on sait déjà

On a vu qu'il était possible de passer des paramètres à un programme CGI. Avec la méthode GET, les paramètres apparaissent dans l'URL. Avec la méthode POST, les paramètres sont cachés (à noter que Internet Explorer 3 est bogué et qu'un « refresh » PERD les paramètres !) Ce qu'il nous manque c'est la possibilité de stocker une information.

VI-B. Comment ça marche ?

Le « cookie » est une information liée à une URL, quand le navigateur web charge une URL, il indique dans une variable CGI (HTTP_COOKIE) la liste des cookies qui lui correspond.

Un cookie a un nom et une valeur, donc HTTP_COOKIE prendra la valeur : NOM1=VALEUR1; NOM2=VALEUR2;…

On va donc avoir plusieurs cookies par URL.

VI-C. Et comment ça se cuisine un cookie ?

Oui, avant de les lire, faut les définir ! Pour ça il suffit d'utiliser l'entête HTTP.

Habituellement on répond un truc genre :

 
Sélectionnez
Content-type: text/html

On va pouvoir ajouter un ordre de création de cookie !

 
Sélectionnez
Content-type: text/html
Set-Cookie: Nom=Valeur

On pourra par exemple s'en servir pour sauvegarder le nom et le mot de passe de l'utilisateur. Les Mygaliens (euh pardon, les « Multimaniac ») connaissent bien ce principe : une page HTML demande le profil/mot de passe en mode POST et l'information est sauvegardée dans un cookie.

VI-D. Exemple d'utilisation d'un cookie

À la demande générale (un mail), j'ai fait un programme exemple d'utilisation des cookies Cook qui montre comment utiliser les cookies pour authentifier un utilisateur.

Ce ZIP contient de plus la dernière version de mon unité CGI et une unité base64.

VI-E. En savoir plus

Pour plus d'information sur le sujet, voir la documentation de Netscape : Client Side State - HTTP Cookies.

C'est tout !

VII. Base de données

VII-A. Comment ça marche ?

L'accès base de données n'est pas propre aux CGI… mais ce contexte implique quelques réflexions.

Il faut bien garder a l'esprit que le CGI est un programme invoqué par un navigateur, qu'il s'exécute sur le serveur, et qu'il DOIT se terminer pour que le serveur et le navigateur considèrent que la requête est terminée… Du coup, tout accès base de données est obligatoirement initialisé à chaque appel au CGI !

Il faut donc absolument optimiser l'ouverture de la base pour avoir un résultat satisfaisant.

Tu peux télécharger ABook, qui est un exemple complet d'accès a une base de données (ici Access) depuis un CGI. Il utilise ODBC avec une unité de orienté objet plutôt simple à utiliser (je trouve)… La base est ouverte puis refermée a chaque requête…

Je n'ai pas testé cette méthode sur un grosse base mais en tout cas l'ouverture d'une base Access directement en ODBC est infiniment plus rapide qu'avec BDE !

Pour une application plus lourde il faudra sans doute exploiter une application « serveur » de données qui tourne en permanence (ou presque) sur le serveur. Le CGI n'aurait alors qu'à envoyer une requête à cette application sans se préoccuper de l'ouverture de la base…

Tu peux même chercher à définir des sessions (en utilisant un cookie par exemple) qui permettraient de conserver des informations entre deux requêtes… Mais n'oublie pas que le client peut très bien naviguer de façon anarchique ! Tu dois t'assurer que tu n'as pas affaire à une nouvelle fenêtre du navigateur ou a une ancienne en cache par exemple…

C'est tout !

VIII. Programmes CGI et Delphi : FAQ

Comment configurer les CGI sur IIS

Lance :

Démarrer/Programmes/Microsoft Internet Server/Gestionnaire des Services

Clique deux fois sur le service WWW, et sélectionne l'onglet « Répertoires » :

Répertoire Alias Adresse erreur
C:\InetPub\wwwroot <Répertoire de base>  
C:\InetPub\scripts /Scripts  
C:\WINNT\System32\inetsrv\iisadmin /iisadmin  

Clique sur Ajouter, indique le répertoire qui contient les programmes CGI (C:\DELPHI par exemple) l'alias de répertoire virtuel est habituellement « /cgi-bin », supprime l'accès en lecture et ajoute l'accès « Exécuter ».

Répertoire Alias Adresse erreur
C:\InetPub\wwwroot <Répertoire de base>  
c:\delphi /cgi-bin  
C:\InetPub\scripts /Scripts  
C:\WINNT\System32\inetsrv\iisadmin /iisadmin  

Il ne reste plus qu'à placer les EXE dans C:\DELPHI (par exemple) et d'invoquer les CGI par http://www.monserver.fr/cgi-bin/programme.exe.

Si NT donne une erreur de droits, clique droit sur le dossier pour vérifier les droits.

VIII-A. Pourquoi est-ce que <a href="/cgi-bin/programme.exe"> me propose-t-il désespérément de CHARGER ou d'EXÉCUTER le programme à la place de faire ce qu'il devrait ?

Pour que le CGI retourne une page Web, il faut impérativement qu'il soit lancé par un serveur Web, il est impossible de lancer un CGI en local (sauf si tu installes un serveur Web en local… Mais il faut alors passer par le serveur - http://127.0.0.1/cgi-bin/programme.exe).

C'est tout !