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…
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 !!!
@
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 :
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 :
@
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 :
<
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.
//
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 :
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 :
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 :
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 :
HTTP/1.0 302 Found
Location: http://www.yahoo.com
Les navigateurs demandent alors l'URL http://www.yahoo.com pour recevoir :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
Content-type: text/html
On va pouvoir ajouter un ordre de création de cookie !
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 !