16. Anatomie d'un serveur Web▲
16-1. Le protocole HTTP▲
ATTENTION CE CHAPITRE N'A PAS FAIT L'OBJET D'UNE REVISION DEPUIS DE NOMBREUSES ANNÉES. LES INFORMATIONS CONTENUES Y SONT JUSTES, MAIS PASSABLEMENT OBSOLÈTES !
Ce document est une présentation succincte du protocole HTTP 1.0 et du serveur Apache qui l'utilise.
Le protocole HTTP(193) est le protocole d'application utilisé pour véhiculer, entre autres, du HTML(194) sur l'Internet.
C'est le protocole le plus utilisé en volume depuis 1995, devant FTP, NNTP et SMTP ; il accompagne l'explosion de l'utilisation du système global d'information « World-Wide Web ».
Depuis 1990, date d'apparition du « Web », le protocole HTTP évolue doucement, mais sûrement. Il est longtemps resté sous la forme de « draft ». La première version déployée largement a été la 1.0, définie dans la RFC 1945 de mai 1996. Depuis le début du mois de janvier 1997 est apparue la version 1.1, deux fois plus volumineuse pour tenir compte des nouvelles orientations de l'usage du service.
Aujourd'hui ce protocole est tellement répandu que pour bon nombre de nouveaux utilisateurs du réseau, l'Internet c'est le « web » !
Techniquement, l'usage de ce protocole se conçoit comme une relation entre un client et un serveur. Le client, appelé génériquement un « browser », un « User Agent », ou encore butineur de toile, interroge un serveur connu par son « URL(195) » dont la syntaxe est bien décrite dans la RFC 1738.
Par exemple la chaîne de caractères http://www.sio.ecp.fr/ est une URL ; il suffit de la transmettre en tant qu'argument à un quelconque outil d'exploration et celui-ci vous renverra (si tout se passe comme prévu !) ce qui est prévu sur le serveur en question pour répondre à cette demande (car il s'agit bien d'une requête comme nous le verrons plus loin dans ce chapitre).
Le serveur, supposé à l'écoute du réseau au moment où la partie cliente l'interroge, utilise un port connu à l'avance. Le port 80 est dédié officiellement au protocole HTTP(196), mais ce n'est pas une obligation (cette décision est prise à la configuration du serveur). L'URL qui désigne un serveur peut contenir dans sa syntaxe le numéro de port sur lequel il faut l'interroger, comme dans :
- http://www.sio.ecp.fr:11235/
16-1-1. Exemple d'échange avec HTTP▲
Le transport des octets est assuré par TCP et le protocole est « human readable », ce qui nous autorise des essais de connexion avec le client TCP à tout faire : Telnet ! Bien entendu on pourrait utiliser un « browser » plus classique, mais celui-ci gérant pour nous le détail des échanges il ne serait plus possible de les examiner.
$ Telnet localhost 80
Trying...
Connected to localhost.
Escape character is '^]'.
Ce qui est tapé par l'utilisateur et la réponse du serveur.
GET / HTTP/1.0
La requête, suivie d'une ligne vide.
HTTP/1.1 200 OK
Date: Fri, 01 Mar 2002 10:59:06 GMT
Server: Apache/1.3.23 (Unix)
Last-Modified: Sat, 10 Nov 2001 16:13:02 GMT
ETag: "1381-8b-3bed520e"
Accept-Ranges: bytes
Content-Length: 79
Connection: close
Content-Type: text/html
<HTML>
<HEAD>
<TITLE>Ceci est un titre</TITLE>
</HEAD>
<BODY>
</BODY>
</HTML>
Connection closed by foreign host.
Enfin la réponse du serveur, que l'on peut décomposer en trois parties :
- Un code de retour (HTTP)Â ;
- Un entête MIME ;
- Des octets, ici ceux d'une page écrite en HTML.
Notons également la déconnexion à l'initiative du serveur, en fin d'envoi de la page HTML.
16-1-2. Structure d'un échange▲
L'exemple qui précède est typique d'un échange entre le client et le serveur : une question du client génère une réponse du serveur, le tout lors d'une connexion TCP qui se termine lors de l'envoi du dernier octet de la réponse (clôture à l'initiative du serveur).
Le serveur ne conserve pas la mémoire des échanges passés, on dit aussi qu'il est sans état, ou « stateless ».
La question et la réponse sont bâties sur un modèle voisin : le message HTTP.
Les parties A, B et C forment l'entête du message, et D le corps.
À la première ligne du message est soit la question posée (« request-line »), soit le statut de la réponse (« status-line »).
- La question est une ligne terminée par CRLF, elle se compose de trois champs : une méthode à prendre dans GET, HEAD, ou POST :
- GET Plus de 99 % des requêtes ont cette méthode, elle retourne l'information demandée dans l'URL (ci-dessous),
- HEAD La même chose que GET, mais seul l'entête du serveur est envoyé. Utile pour faire des tests d'accessibilité sans surcharger la bande passante. Utile également pour vérifier de la date de fraicheur d'un document (information contenue dans l'entête),
- POST Cette méthode permet d'envoyer de l'information au serveur, c'est typiquement le contenu d'un formulaire rempli par l'utilisateur ;
- une ressource que l'on désigne par une URL(197). Par exemple http://www.site.org/;
- la version du protocole, sous la forme HTTP-Numéro de version. Par exemple HTTP/1.1 !
- La réponse. Cette première ligne n'est que le statut de la réponse, les octets qui la détaillent se trouvent plus loin, dans le corps du message. Trois champs la composent, elle se termine par CRLF :
- La version du protocole, sous forme HTTP-Numéro de version, comme pour la question.
- Statut C'est une valeur numérique qui décrit le statut de la réponse. Le premier des trois digits donne le sens général :
- 1xx N'est pas utilisé « Futur Use »
- 2xx Succès, l'action demandée a été comprise et exécutée correctement.
- 3xx Redirection. La partie cliente doit reprendre l'interrogation, avec une autre formulation.
- 4xx Erreur côté client. La question comporte une erreur de syntaxe ou ne peut être acceptée.
- 5xx Erreur côté serveur. Il peut s'agir d'une erreur interne, due à l'OS ou à la ressource devenue non accessible.
- Phrase C'est un petit commentaire (« Reason- Phrase ») qui accompagne le statut, par exemple le statut 200 est suivi généralement du commentaire « OK » !
B C'est une partie optionnelle, qui contient des informations à propos du corps du message. Sa syntaxe est proche de celle employée dans le courrier électronique, et pour cause, elle respecte aussi le standard MIME(198).
- Un entête de ce type est constitué d'une suite d'une ou plusieurs lignes (la fin d'une ligne est le marqueur CRLF) construite sur le modèle :
- Nom de champ : Valeur du champ CRLF
- éventuellement le marqueur de fin de ligne peut être omis pour le séparateur « ; ».
- Exemple d'entête MIME :
2.
3.
4.
5.
6.
7.
8.
Date: Fri, 01 Mar 2002 10:59:06 GMT
Server: Apache/1.3.23 (Unix)
Last-Modified: Sat, 10 Nov 2001 16:13:02 GMT
ETag: "1381-8b-3bed520e"
Accept-Ranges: bytes
Content-Length: 79
Connection: close
Content-Type: text/html
- Date : C'est la date à laquelle le message a été envoyé. Bien sûr il s'agit de la date du serveur, il peut exister un décalage incohérent si les machines ne sont pas synchronisées (par exemple avec XNTP).
- Server : Contient une information relative au serveur qui a fabriqué la réponse. En général, la liste des outils logiciels et leur version.
- Content-type : Ce champ permet d'identifier les octets du corps du message.
- Content-length : Désigne la taille (en octets) du corps du message, c'est-à -dire la partie D de la figure XV.1.
- Last-modified : Il s'agit de la date de dernière modification du fichier demandé, comme l'illustre le résultat de la commande ll (voir aussi la coïncidence de la taille du fichier et la valeur du champ précédent).
-rw-r--r-- 1 web doc 139 Nov 10 17:13 index.html
- ETag : C'est un identificateur du serveur, constant lors des échanges. C'est un moyen de maintenir le dialogue avec un serveur en particulier, par exemple quand ceux-ci sont en grappe pour équilibrer la charge et assurer la redondance.
C Une ligne vide (CRLF) qui est le marqueur de fin d'entête. Il est donc absolument obligatoire qu'elle figure dans le message. Son absence entraîne une incapacité de traitement du message, par le serveur ou par le client.
D Le corps du message. Il est omis dans certains cas, comme une requête avec la méthode GET ou une réponse à une requête avec la méthode HEAD.
- C'est dans cette partie du message que l'on trouve par exemple les octets du HTML, ou encore ceux d'une image...
- Le type des octets est intimement lié à celui annoncé dans l'entête, plus précisément dans le champ Content-Type.
- Par exemple :
Content-Type : text/html =) Le corps du message contient des octets à interpréter comme ceux d'une page écrite en HTML.
Content-Type : image/jpg =) Le corps du message contient des octets à interpréter comme ceux d'une image au format jpeg
16-2. URI et URL▲
Le succès du « web » s'appuie largement sur un système de nommage des objets accessibles, qui en uniformise l'accès, qu'ils appartiennent à la machine sur laquelle on travaille ou distants sur une machine en un point quelconque du réseau (mais supposé accessible). Ce système de nommage universel est l'URL (« Uniform Resource Locator » - RFC 1738) dérivé d'un système de nommage plus général nommé URI (« Universal Resource Identifier » - RFC 1630).
La syntaxe générale d'une URL est de la forme :
- <scheme>Â :<scheme-specific-part>
Succintement la « scheme » est une méthode que l'on sépare à l'aide du caractère « : » d'une chaîne de caractères ASCII 7 bits dont la structure est essentiellement fonction de la « scheme » qui précède et que l'on peut imaginer comme un argument.
Une « scheme » est une séquence de caractères 7 bits. Les lettres « a » à « z », les chiffres de « 0 » à « 9 », le signe « + », le « . » et le « - » sont admis. Majuscules et minuscules sont indifférenciées.
Exemples de « schemes » : http, FTP, file, mailto... Il en existe d'autres (cf la RFC) non indispensables pour la compréhension de cet exposé.
Globalement une URL doit être encodée en ASCII 7 bits(199) sans caractère de contrôle (c'est-à -dire entre les caractères 20 et 7F), ce qui a comme conséquence que tous les autres caractères doivent être encodés.
La méthode d'encodage transforme tout caractère non utilisable directement en un triplet formé du caractère « % » et de deux caractères qui en représente la valeur hexadécimale. Par exemple l'espace (20 hex) doit être codé .
Un certain nombre de caractères, bien que théoriquement représentables, sont considérés comme non sûrs (« unsafe ») et devraient être également encodés de la même manière que ci-dessus, ce sont :
- % < > " # { } | \ ^ ~ [ ] `
Pour un certain nombre de « schemes » (http...) certains caractères sont réservés car ils ont une signification particulière. Ce sont :
- ; / ? : @ = &
Ainsi, s'ils apparaissent dans l'URL sans faire partie de sa syntaxe, ils doivent être encodés.
16-2-1. Scheme HTTP▲
Une URL avec la « scheme » HTTP bien formée doit être de la forme :
- http://$<$host$>$:$<$port$>$/$<$path$>$?$<$searchpath$>$
- « path » et « searchpath » sont optionnels.
- host C'est un nom de machine ou une adresse IP.
- port Le numéro de port. S'il n'est pas précisé, la valeur 80 est prise par défaut.
- path C'est un sélecteur au sens du protocole HTTP.
- searchpath C'est ce que l'on appelle la « query string », autrement dit la chaîne d'interrogation.
- À l'intérieur de ces deux composantes, les caractères / ; ? sont réservés, ce qui signifie que s'ils doivent être employés, ils doivent être encodés pour éviter les ambiguïtés.
- « path » et « searchpath » sont optionnels.
- host C'est un nom de machine ou une adresse IP.
- port Le numéro de port. S'il n'est pas précisé, la valeur 80 est prise par défaut.
- path C'est un sélecteur au sens du protocole HTTP.
- searchpath C'est ce que l'on appelle la « query string », autrement dit la chaîne d'interrogation.
- À l'intérieur de ces deux composantes, les caractères / ; ? sont réservés, ce qui signifie que s'ils doivent être employés, ils doivent être encodés pour éviter les ambiguïtés.
Le ? marque la limite entre l'objet interrogeable et la « query string ». À l'intérieur de cette chaîne d'interrogation, le caractère + est admis comme raccourci pour l'espace (ASCII 20 hex). Il doit donc être encodé s'il doit être utilisé en tant que tel.
De même, à l'intérieur de la « query string » le caractère = marque la séparation entre variable et valeur, le & marque la séparation entre les couples variable = valeur.
Exemple récapitulatif :
http://www.google.fr/search?q=cours+r?seaux\&hl=fr\&start=10\&sa=N
Notez le « é » codé ?, c'est-à -dire le caractère de rang 14x16+9 = 233.
On peut également observer quatre variables q, hl, start et sa dont la signification peut être partiellement devinée, mais dont le remplissage reste à la charge du serveur en question.
Le rôle de la chaîne « search » est celui de ce que l'on appelle une CGI ou « Common Gateway Interface », c'est-à -dire un programme qui effectue le lien entre le serveur interrogé et des programmes d'application, ici une recherche dans une base de données. Nous examinons succinctement le principe de fonctionnement de tels programmes.
16-3. Architecture interne du serveur Apache▲
Cette partie se consacre à l'étude du fonctionnement du serveur Apache(200).
Pour comprendre les mécanismes internes d'un tel serveur il est absolument nécessaire de pouvoir en lire le code source, cette contrainte exclut les produits commerciaux.
Au mois de mars 2002, d'après le « Netcraft Web Server Survey(201) » le serveur le plus utilisé (55 %) est très majoritairement celui du projet Apache.
D'après ses auteurs, le serveur Apache est une solution de continuité au serveur du NCSA(202). Il corrige des bogues et ajoute de nombreuses fonctionnalités, particulièrement un mécanisme d'API pour permettre aux administrateurs de sites de développer de nouveaux modules adaptés à leurs besoins propres.
Plus généralement, tout ce qui n'est pas strictement dans les attributions du serveur (gestion des processus, gestion mémoire, gestion du réseau) est traité comme un module d'extension. Le fichier apache 1.3.23/htdocs/manual/misc/API.html de la distribution standard apporte des précisions, le serveur http://www.apacheweek.com/ pointe également sur grand nombre de documents très utiles.
Le serveur Apache introduit également la possibilité de serveurs multidomaines (domaines virtuels), ce qui est fort apprécié des hébergeurs de sites.
16-3-1. Environnement d'utilisation▲
La figure XV.2 qui suit, synthétise l'environnement d'utilisation.
Le serveur se met en œuvre simplement. La compilation fournit un exécutable, httpd, qui, pour s'exécuter correctement, a besoin des trois fichiers ASCII de configuration : srm.conf, access.conf, httpd.conf.
C'est en fait dans celui-ci que sont effectués l'essentiel des ajustements locaux à la configuration standard.
Lors de l'exécution, trois fichiers sont modifiés(203) :
- httpd.pid (PidFile) contient le « process ID » du « leader » de groupe ; à utiliser avec le signal SIGHUP (cf figure XV.2) pour provoquer la relecture et le redémarrage « à chaud » du serveur, ou avec SIGTERM pour mettre fin à son activité.
- access log (CustomLog) Qui contient le détail des accès clients. Ce fichier peut devenir très volumineux.
- error log (ErrorLog) Qui contient le détail des accès infructueux et des éventuels problèmes de fonctionnement du serveur.
Le « daemon » httpd est soit du type (ServerType) « standalone » ou si son invocation est occasionnelle, à la demande piloté par inetd.
Il démarre son activité par ouvrir le port (Port) désigné dans la configuration, puis s'exécute avec les droits de l'utilisateur User et du groupe Group. Sa configuration se trouve dans le répertoire conf sous-répertoire de ServerRoot. Les fichiers accessibles par les connexions clientes, eux, se situent dans le répertoire DocumentRoot qui en est la racine, c'est-à -dire vue comme »/ » par les browsers clients.
Le fichier httpd.pid contient un numéro de processus « leader » de groupe car en fait le serveur Apache, dès son initialisation, démarre un certain nombre de processus, autant de serveurs capables de comprendre les requêtes des clients. En voici un exemple :
- MinSpareServers 5 C'est le nombre minimum d'instances du serveur (non compris le processus maître) en attente d'une connexion. S'il en manque, elles sont créées.
- MaxSpareServers 10 C'est le nombre maximum d'instances du serveur (non compris le processus maître) en attente d'une connexion. S'il y en a de trop elles sont supprimées.
- StartServers 5 C'est le nombre minimum d'instances du serveur (non compris le processus maître) au démarrage.
- MaxClients 150 C'est le nombre maximum de clients (requêtes HTTP) simultanés. Cette constante peut être augmentée en fonction de la puissance de la machine.
Un processus joue le rôle de régulateur, du point de vue Unix c'est un processus chef de groupe (« leader »). La commande ps permet de visualiser une situation opérationnelle :
web 17361 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
root 2794 1 0 Feb 23 ? 0:06 /usr/local/bin/httpd -d /users/web
web 17363 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
web 17270 2794 0 Mar 13 ? 0:01 /usr/local/bin/httpd -d /users/web
web 17362 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
web 17401 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
web 17313 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
web 17312 2794 0 Mar 13 ? 0:01 /usr/local/bin/httpd -d /users/web
web 17355 2794 0 Mar 13 ? 0:00 /usr/local/bin/httpd -d /users/web
web 17314 2794 0 Mar 13 ? 0:01 /usr/local/bin/httpd -d /users/web
Ici il y a neuf instances du serveur et le processus maître, qu'il est aisé de reconnaitre (2794) car il est le père des autres processus (ce qui n'implique pas qu'il en est le chef de groupe, mais la suite de l'analyse nous le confirmera).
La commande netstat -f inet -a | grep http ne donne qu'une ligne :
TCP 0 0 *.http *.* LISTEN
Cela signifie qu'aucune connexion n'est en cours sur ce serveur, et qu'une seule « socket » est en apparence à l'écoute du réseau. C'est une situation qui peut sembler paradoxale eu égard au nombre de processus ci-dessus, le code nous fournira une explication au paragraphe suivant.
La commande tail -1 logs/access.log fournit la trace de la dernière requête :
www.chezmoi.tld - - [01/Mar/2002:17:13:28 +0100] "GET / HTTP/1.0" 200 79
Il s'agit de la trace de notre exemple d'interrogation du début de ce chapitre !
16-3-2. Architecture interne▲
Attention, ce paragraphe concerne la version 1.1.1 du logiciel. Le fonctionnement de la version courante, 1.3.23, reste similaire, mais le code ayant beaucoup changé, les numéros de lignes sont devenus de fait complètement faux.
Ce qui nous intéresse plus particulièrement pour expliquer le fonctionnement du serveur se trouve dans le répertoire src/, sous répertoire du répertoire principal de la distribution :
$ ll apache_1.1.1/
total 19
-rw------- 1 fla users 3738 Mar 12 1996 CHANGES
-rw------- 1 fla users 2604 Feb 22 1996 LICENSE
-rw------- 1 fla users 3059 Jul 3 1996 README
drwxr-x--- 2 fla users 512 Feb 7 22:14 CGI-bin
drwxr-x--- 2 fla users 512 Feb 7 22:14 conf
drwxr-x--- 2 fla users 512 Feb 7 22:14 htdocs
drwxr-x--- 2 fla users 2048 Feb 7 22:14 icons
drwxr-x--- 2 fla users 512 Jul 8 1996 logs
drwxr-x--- 2 fla users 2048 Mar 12 10:42 src
drwxr-x--- 2 fla users 512 Feb 7 22:15 support
Dans ce répertoire qui compte au moins 75 fichier (wc -l *.[ch] ) 27441 lignes) nous nous restreignons aux fichiers essentiels décrits dans le « README » soit encore une petite dizaine d'entre eux (wc -l ) 6821 lignes) : mod CGI.c, http protocol.c, http request.c, http core.c, http config.c, http log.c, http main.c, alloc.c
Dans un premier temps nous allons examiner le fonctionnement de la gestion des processus, du mode de relation entre le père et ses fils.
Puis, dans un autre paragraphe, nous examinerons plus particulièrement ce qui se passe lors d'une connexion, avec le cas particulier de l'exécution d'une CGI(204) qui comme son nom l'indique est le programme qui effectue l'interface entre le serveur HTTP et un programme d'application quelconque.
Dans la suite de ce document, le terme « CGI » employé seul désigne ce type d'application.
16-3-2-1. Gestion des processus▲
Au commencement est le main, et celui-ci se trouve dans le fichier http main.c, comme il se doit !
... ...
1035
pool *
pconf; /* Pool for config stuff */
1036
pool *
ptrans; /* Pool for per-transaction stuff */
... ...
1472
int
1473
main
(
int
argc, char
*
argv[])
1474
{
... ...
1491
init_alloc
(
);
1492
pconf =
permanent_pool;
1493
ptrans =
make_sub_pool
(
pconf);
La fonction init alloc appelle make sub pool qui initialise un intéressant mécanisme de buffers chaînés, utilisé tout au long du code dès lors qu'il y a besoin d'allouer de la mémoire.
Les différents modules du serveur sont ajoutés dans une liste chaînée.
... ...
1523
setup_prelinked_modules
(
);
1524
1525
server_conf =
read_config (
pconf, ptrans, server_confname);
1526
1527
if
(
standalone) {
1528
clear_pool (
pconf); /* standalone_main rereads... */
1529
standalone_main
(
argc, argv);
1530
}
1531
else
{
... ...
1580
}
1581
exit (
0
);
1582
}
« Standalone » est à 0 si le serveur est invoqué depuis inetd(205). Son mode de fonctionnement le plus efficace reste avec ce paramètre à 1 (voir le cours sur inetd), que nous supposons ici.
- La fonction standalone main (ligne 1362) prépare le processus à son futur rôle de serveur. Pour bien comprendre le cette fonction, il faut imaginer qu'elle est invoquée au démarrage, et « à chaud », pour lire et relire la configuration.
- Ligne 1369 , la variable one process = 0 (sinon le processus est en mode debug) occasionne l'appel à la fonction detach (ligne 876). Celle-ci transforme le processus en « leader » de groupe avec la succession bien connue fork + setsid (pour plus de détails, voir le cours sur les daemons).
- Ligne 1374 , la fonction sigsetjmp enregistre la position de la pile et l'état du masque d'interruption (deuxième argument non nul) dans restart buffer. Tout appel ultérieur à siglongjmp forcera la reprise de l'exécution en cette ligne.
- Ligne 1377 On ignore tout signal de type SIGHUP, cela bloque toute tentative cumulative de relecture de la configuration.
- Ligne 1382 (one process = 0) on envoie un signal SIGHUP à tous les processus du groupe. Cette disposition n'est utile que dans le cas d'un redémarrage « à chaud ». La variable pgrp est initialisée par la fonction.
- detach (ligne 876), elle contient le PID du chef de groupe. L'intérêt d'avoir un groupe de processus est que le signal envoyé à son « leader » est automatiquement envoyé à tous les processus y appartenant. Chaque processus qui reçoit ce signal, s'il est en mesure de le traiter, appelle la fonction just die qui exécute un exit(0) (ligne 943). Donc tous les fils meurent, sauf le chef de groupe.
- Ligne 1390 , l'appel à la fonction reclaim child processes() effectue autant de wait qu'il y avait de processus fils, pour éviter les zombis.
- Ligne 1398 Relecture des fichiers de configuration.
- Ligne 1400 set group privs (ligne 960) change le « user id » et le « group id » si ceux-ci ont été modifiés.
- Ligne 1401 accept mutex init (ligne 166) fabrique un fichier temporaire (/usr/tmp/htlock.XXXXXX), l'ouvre, réduit son nombre de liens à 0, de telle sorte qu'il sera supprimé dès la fin du processus. Ce fichier est le verrou d'accès à la socket principale, comme nous l'examinerons un peu plus loin.
- Ligne 1402 reinit scoreboard (ligne 596) Cette fonction remet à zéro, ou crée (setup shared mem ligne 432) la zone de mémoire commune entre le chef de groupe et ses processus fils. Cette zone mémoire est, soit un segment de mémoire partagée (IPC du système V), soit de la mémoire rendue commune par un appel à la primitive mmap (Le choix est effectué par configure qui analyse les possibilités du système, avant compilation du serveur). La taille de cette zone est de HARD SERVER LIMIT x sizeof(short score) octets, la structure short score, définie dans scoreboard.h, recueille les statistiques d'utilisation de chaque serveur et son statut (par exemple SERVER READY, SERVER DEAD...). C'est par ce moyen que le serveur maître contrôle les serveurs fils. HARD SERVER LIMIT définit le nombre maximum de connexions actives, donc d'instances de serveur, à un instant donné. En standard cette valeur est 150, elle peut être augmentée dans le fichier de configuration httpd.conf (voir ci-dessus au paragraphe II.1)
Enfin la figure XV.4 montre son rôle stratégique.
- Ligne 1413 (on suppose listeners = NULL), après avoir initialisé une structure d'adresse, appel à la fonction make sock (ligne 1298). Celle-ci crée et initialise une socket, et, détail important, appelle par deux fois setsockopt, ce qui mérite un commentaire :
... ...
1312
if
((
setsockopt
(
s, SOL_SOCKET,SO_REUSEADDR,(
char
*
)&
one,sizeof
(
one)))
1313
==
-
1
) {
... ...
1318
if
((
setsockopt
(
s, SOL_SOCKET,SO_KEEPALIVE,(
char
*
)&
keepalive_value,
1319
sizeof
(
keepalive_value))) ==
-
1
) {
... ...
-
- SO REUSEADDR Indique que la règle d'exclusivité suivie par bind(2) pour l'attribution d'un port ne s'applique plus : un processus peut se servir d'un même port pour plusieurs usages différents (par exemple le client FTP qui attaque les ports 21 et 20 du serveur avec le même port local), voire des processus différents (c'est le cas ici) peuvent faire un bind avec le même port sans rencontrer la fatidique erreur 48 (« Address already in use ») !
Vu des clients HTTP, le serveur est accessible uniquement sur le port 80, ce que nous avons remarqué au paragraphe II.1 (netstat) sans l'expliquer, voilà qui est fait !
-
- SO KEEPALIVE Indique à la couche de transport, ici TCP, qu'elle doit émettre à intervalle régulier (non configurable) un message à destination de la socket distante. Que celle-ci n'y réponde pas et la prochaine tentative d'écriture sur la socket se solde par la réception d'un SIGPIPE, indiquant la disparition de la socket distante (voir plus loin la gestion de ce signal dans la fonction child main, ligne 1139).
- Ligne 1430 set signals (1007) prévoit le comportement lors de la réception des signaux :
- SIGHUP Appel de restart
- SIGTERM Appel de sig term
- Ligne 1438 et la suivante, création d'autant de processus fils qu'il en est demandé dans le fichier de configuration (StartServers). La fonction make child (1275) appelle fork, puis dans le fils modifie le comportement face aux signaux SIGHUP et SIGTERM (just die appelle exit) avant d'exécuter child main.
Arrivés à ce stade, il nous faut analyser l'attitude des deux types de processus.
Le processus maître
- Ligne 1444 démarre une boucle infinie de surveillance. Seule la réception et le traitement d'un signal peuvent l'interrompre.
- Ligne 1458 Ici, si le nombre de serveurs disponibles est inférieur au nombre minimal requis, il y régénération de ceux qui manquent avec la fonction make child.
Les processus esclaves
- Ligne 1139 La gestion de ces processus, autant de serveurs Web opérationnels, débute avec l'appel de la fonction child main.
- Ligne 1167 Début de la boucle infinie qui gère cette instance du serveur. Au cours d'une boucle le serveur gère l'acceptation d'une requête et son traitement.
- Ligne 1174 Gestion du SIGPIPE donc des clients qui déconnectent avant l'heure !
- Ligne 1180 Si le nombre de serveurs libres (count idle servers) est supérieur à la limite configurée, ou si
- Ligne 1182 le nombre de requêtes traitées par ce processus a atteint la limite max requests per child, le processus s'arrête de lui-même. C'est l'autorégulation pour libérer des ressources occupées par un trop grand nombre de processus serveurs inutiles.
- Ligne 1190 L'appel à la fonction accept mutex on verrouille l'accès à la ressource définie précédemment avec accept mutex init (ligne 166). Ce verrouillage est bloquant et exclusif. C'est-à -dire qu'il faut qu'un autre processus en déverrouille l'accès pour que le premier processus sur la liste d'attente (gérée par le noyau Unix) y accède.
Ce fonctionnement est assuré suivant la version d'Unix par la primitive flock ou par la primitive fcntl.
Les sémaphores du Système V (semget, semctl, semop...) assurent la même fonctionnalité, en plus complet, ils sont aussi plus complexes à mettre en œuvre.
Cette opération est à rapprocher de l'option SO REUSEADDR prise ligne 1312. Il faut éviter que plusieurs processus serveurs disponibles ne répondent à la requête. Il n'y a qu'un seul processus prêt à répondre à la fois et dès que le accept (ligne 1217) retourne dans le code utilisateur la ressource est déverrouillée (on suppose toujours listeners = 0).
- Ligne 1221 La fonction accept mutex off déverrouille l'accès à la socket.
- Ligne 1245 read request lit la requête du client, donc un message HTTP.
- Ligne 1247 process request fabrique la réponse au client, donc un autre message HTTP.
16-3-2-2. Prise en main des requêtes▲
Le fichier concerné par la lecture de la requête est http protocol.c.
La lecture de la première ligne du message HTTP est assurée par la fonction read request line, ligne 329(206).
La lecture de l'entête MIME est assurée par la fonction get mime headers, ligne 356. Attention, seul l'entête est lu, le corps du message dans le cas de la méthode POST est lu plus tard, lors du traitement du message, par le programme d'application (CGI).
Le fichier concerné par la formulation de la réponse est http request.c et la fonction principale process request, ligne 772. Celle-ci appelle en cascade process request internal, ligne 684.
Cette dernière effectue un certain nombre de tests avant de traiter effectivement la requête. Parmi ceux-ci on peut relever,
- Ligne 716 La fonction unescape url (util.c, ligne 593) assure le décodage des caractères réservés et transcodés comme il est spécifié par la RFC 1738.
- Ligne 723 La fonction getparents filtre les chemins (« pathname ») qui prêtent à confusion.
- Ligne 768 La fonction invoke handler est le point d'entrée dans le traitement de la requête. Cette fonction (http config.c, ligne 267) invoque le programme (module) qui se charge de fabriquer la réponse, au vu du contenu du champ content type de la requête. Si celui est inexistant, comme dans notre exemple du paragraphe I, il est positionné par défaut à la valeur html/text.
16-3-2-3. Deux types de CGI▲
Pour la suite nous supposons que la prise en main de la requête est faite par le module « CGI », défini dans le fichier mod CGI.c. Le point d'entrée est la fonction CGI handler (ligne 207), c'est-à -dire celle qui appelée par invoke handler, vue au paragraphe ci-dessus.
La lecture du code permet de déduire qu'il y a deux types de CGI, la distinction est faite par le nom de la CGI elle-même.
La figure XV.5 résume les 2 situations possibles d'exécution d'une CGI.
Si le nom de l'exécutable commence par nph-(207) le comportement du serveur http change. Dans le cas ordinaire (nom quelconque) les données transmises depuis le client vers la CGI et réciproquement, passent par le processus httpd et via un tube non nommé (« pipe »).
Dans le cas d'une CGI « nph », seules les données lues depuis le client (par exemple avec la méthode POST) transitent par le processus httpd, la réponse, elle, est émise directement, ce qui améliore les performances en évitant une séquence lecture/écriture inutile. Ce comportement est justifié dès que de gros volumes d'octets sont à transmettre au client (de grandes pages HTML, des images...).
Attention, dans ce dernier cas, c'est à la CGI de fabriquer l'intégralité du message HTTP, y compris l'entête MIME. À elle également de gérer la déconnexion prématurée du client (SIGPIPE).
Ces deux modes de fonctionnement ne sont pas clairement documentés, en fait il s'agit d'une caractéristique du serveur du CERN, maintenue pour assurer sans doute la compatibilité avec les applicatifs déjà écrits. Il n'est pas assuré que cette possibilité existera dans les futures versions du serveur « Non Parse Header » Apache, notamment celles qui utiliseront la version 1.1 d'HTTP.
Examinons le code du fichier mod CGI.c :
207
int
CGI_handler (
request_rec *
r)
208
{
209
int
nph;
... ...
222
nph =
!(
strncmp
(
argv0,"
nph-
"
,4
));
... ...
La variable nph vaut 1
si la CGI est de ce type.
... ...
251
add_common_vars (
r);
... ...
Ici on commence à fabriquer l'environnement d'exécution de la CGI Cette fonction (fichier util script.c, ligne 126) complète les variables qui ne dépendent pas du contenu de la requête, par exemple SERVER SOFTWARE, REMOTE HOST...
... ...
277
if
(!
spawn_child (
r->
connection->
pool, CGI_child, (
void
*
)&
cld,
278
nph ? just_wait : kill_after_timeout,
279
&
script_out, nph ? NULL
: &
script_in)) {
280
log_reason (
"
couldn't spawn child process
"
, r->
filename, r);
281
return
SERVER_ERROR;
282
}
... ...
L'appel de cette fonction provoque la création d'un processus fils, celui qui finalement va exécuter la CGI. Il faut remarquer le deuxième argument qui est un pointeur de fonction (CGI child), et le sixième qui est nul dans le cas d'une CGI du type « nph ».
script in et script out sont respectivement les canaux d'entrée et sortie des données depuis et vers le processus qui exécute la CGI. Il parait donc logique que dans le cas d'une CGI de type « nph » script in soit nul. Un mécanisme non encore analysé duplique la sortie de ce processus vers le client plutôt que vers le processus serveur.
Nous continuons la description du point de vue du processus père, donc httpd.
Ligne 295 et les suivantes, jusqu'à la ligne 332, le processus lit ce que le client lui envoie, si la méthode choisie est du type POST. Le contenu est renvoyé vers le processus fils, sans transformation :
311
if
(
fwrite (
argsbuffer, 1
, len_read, script_out) ==
0
)
312
break
;
... ...
335
pfclose (
r->
connection->
pool, script_out);
Il est donc clair que c'est à celui-ci d'exploiter ce qu'envoie le client, par exemple le résultat d'une forme de saisie.
337
/* Handle script return... */
338
if
(
script_in &&
!
nph) {
... ...
373
send_http_header
(
r);
374
if
(!
r->
header_only) send_fd (
script_in, r);
375
kill_timeout (
r);
376
pfclose (
r->
connection->
pool, script_in);
377
}
Ce test traite le cas d'une CGI normale, dont la sortie est lue par le serveur, puis renvoyée au client (ligne 374).
Examinons maintenant comment se prépare et s'exécute le processus fils :
101
void
CGI_child (
void
*
child_stuff)
102
{
... ...
126
add_CGI_vars (
r);
127
env =
create_environment (
r->
pool, r->
subprocess_env);
Ces deux dernières lignes préparent l'environnement du futur processus fils. Il s'agit du tableau de variables accessibles depuis la variable externe environ, pour tout processus. La fonction add CGI vars (fichier util script.c, ligne 192) ajoute, entre autres, les variables REQUEST METHOD et QUERY STRING à l'environnement.
Cette dernière variable joue un rôle majeur dans la transmission des arguments à la CGI quand la méthode choisie est GET. En effet, dans ce cas, le seul moyen pour le client d'envoyer des arguments à la CGI est d'utiliser l'URL, comme dans :
- http://monweb.chez.moi/CGI-bin/nph-qtp?base=datas\&mot=acacia\&champ=.MC
La syntaxe de l'URL prévoit le caractère « ? » comme séparateur entre le nom et ses arguments. Chaque argument est ensuite écrit sous la forme :
- nom = valeur
Les arguments sont séparés les uns des autres par le caractère « & ».
135
error_log2stderr (
r->
server);
136
... ...
138
if
(
nph) client_to_stdout (
r->
connection);
Ligne 135, la sortie d'erreur est redirigée vers le fichier error log, et ligne 138, la sortie standard du processus, c'est la socket, donc envoyée directement vers le client !
184
if
((!
r->
args) ||
(!
r->
args[0
]) ||
(
ind
(
r->
args,'
=
'
) >=
0
))
185
execle
(
r->
filename, argv0, NULL
, env);
186
else
187
execve
(
r->
filename, create_argv
(
r->
pool, argv0, r->
args), env);
Puis finalement on exécute la CGI ! Bien sûr, si le programme va au-delà de la ligne 187, il s'agit d'une erreur...
16-4. Principe de fonctionnement des CGI▲
16-4-1. CGI - Méthode GET, sans argument▲
Dans ce paragraphe nous examinons ce qui se passe lors de l'exécution d'une CGI très simple (shell script, le source suit) que l'on suppose placée dans le répertoire idoine, pour être exécutée comme lors de la requête suivante :
$ telnet localhost 80
Trying ...
Connected to localhost.
Escape character is '^]'.
GET /CGI-bin/nph-test-CGI HTTP/1.0
HTTP/1.0 200 OK
Content-type: text/plain
Server: Apache/1.1.1
L'entête du message HTTP renvoyé par le serveur. La ligne de statut est générée par la CGI car il s'agit d'une CGI de type nph-
CGI/1.0 test script report:
argc is 0. argv is .
SERVER_SOFTWARE = Apache/1.1.1
SERVER_NAME = www.chezmoi.tld
GATEWAY_INTERFACE = CGI/1.1
SERVER_PROTOCOL = HTTP/1.0
SERVER_PORT = 80
REQUEST_METHOD = GET
SCRIPT_NAME = /CGI-bin/nph-test-CGI
QUERY_STRING =
REMOTE_HOST = labas.tresloin.tld
REMOTE_ADDR = 192.168.0.1
REMOTE_USER =
CONTENT_TYPE =
CONTENT_LENGTH =
Connection closed by foreign host.
Le corps du message. Ces octets sont générés dynamiquement par le programme nph-test-CGI.
Et voici le source de cet exemple (une modification du script de test livré avec Apache) :
#!/bin/sh
echo HTTP/1.0 200 OK
echo Content-type: text/plain
echo Server: $SERVER_SOFTWARE
echo
echo CGI/1.0 test script report:
echo
echo argc is $#. argv is "$*".
echo
Remarquez la fabrication de l'entête MIME, réduite, mais suffisante. Le echo seul génère une ligne vide, celle qui marque la limite avec le corps du message.
echo SERVER_SOFTWARE = $SERVER_SOFTWARE
echo SERVER_NAME = $SERVER_NAME
echo GATEWAY_INTERFACE = $GATEWAY_INTERFACE
echo SERVER_PROTOCOL = $SERVER_PROTOCOL
echo SERVER_PORT = $SERVER_PORT
echo REQUEST_METHOD = $REQUEST_METHOD
echo QUERY_STRING = $QUERY_STRING
echo REMOTE_HOST = $REMOTE_HOST
echo REMOTE_ADDR = $REMOTE_ADDR
echo REMOTE_USER = $REMOTE_USER
echo CONTENT_TYPE = $CONTENT_TYPE
echo CONTENT_LENGTH = $CONTENT_LENGTH
Toutes ces variables sont celles de l'environnement généré par le module CGI handler avant d'effectuer le exec.
16-4-2. CGI - Méthode GET, avec arguments▲
Examinons maintenant un cas plus compliqué, avec des arguments transmis, ou « query string ». Celle-ci est transmise à la CGI via la variable d'environnement QUERY STRING. C'est à la CGI de l'analyser puisque son contenu relève de l'applicatif et non du serveur lui-même. Essayons de coder la CGI de l'exemple :
- http://www.chezmoi.tld/CGI-bin/query?base=datas\&mot=acacia\&champ=.MC
Première version :
#!/bin/sh
echo Content-type: text/plain
echo
echo "QUERY_STRING=>"$QUERY_STRING "<="
exit 0
L'interrogation avec un Telnet produit la sortie :
QUERY_STRING=>base=datas&mot=acacia&champ=.MC<=
Se trouve très facilement sur les sites FTP le source d'un outil nommé CGIparse(208), parfaitement adapté à l'analyse de la chaîne transmise. D'où la deuxième version :
#!/bin/sh
CGIBIN=~web/CGI-bin
BASE=?$CGIBIN/CGIparse -value base?
MOT=?$CGIBIN/CGIparse -value mot?
CHAMP=?$CGIBIN/CGIparse -value champ?
Cette partie du code montre l'analyse du contenu de la variable QUERY STRING avec l'outil CGIparse.
echo Content-type: text/plain
echo
echo "BASE="$BASE
echo "MOT="$MOT
echo "CHAMP="$CHAMP
exit 0
Et là , la fabrication du message renvoyé. La CGI renvoie ses octets via un tube au serveur, c'est donc celui-ci qui se charge de fabriquer un entête MIME.
Puis le résultat de l'exécution :
BASE=datas
MOT=acacia
CHAMP=.MC
16-4-3. CGI - Méthode POST▲
La méthode POST autorise un client à envoyer au serveur une liste de couples variable=valeur. Chaque couple est séparé des autres par une fin de ligne, c'est-à -dire (CR,LF).
Cette méthode est bien adaptée à la transmission d'informations collectées côté client dans une forme(209) de saisie, et dont le volume est variable.
La liste des couples est écrite dans le corps du message, par le programme client, et ne fait pas partie de l'URL, comme c'est le cas pour la méthode GET. Il y a évidemment une limite au nombre maximum d'octets envoyé par le client, c'est le serveur qui en fixe la valeur(210). Du côté du client, il faut prévenir le serveur, dans l'entête du message HTTP, de la taille du corps du message. Cela s'effectue avec le champ Content-length :.
L'exemple suivant ne fait que renvoyer la liste des couples lus, vers le client. Attention il ne fonctionne qu'avec Telnet.
$ telnet www.chezmoi.tld 80
Trying 192.168.0.2...
Connected to www.chezmoi.tld.
Escape character is '^]'.
POST /CGI-bin/test-post HTTP/1.0
Content-length:14
areuh=tagada
Le corps du message fait bien 14 caractères si on compte la fin de ligne (CR+LF).
HTTP/1.0 200 OK
Date: Mon, 24 Mar 1997 14:41:26 GMT
Server: Apache/1.1.1
Content-type: text/plain
REQUEST_METHOD = POST
CONTENT_LENGTH = 14
Couple lu : areuh=tagada
Connection closed by foreign host.
La réponse du serveur liste les couples lus, ici un seul ! La variable globale REQUEST METHOD pourrait être utilisée pour adapter le comportement de la CGI en fonction de la méthode demandée.
Et voici le source de la CGIÂ :
#!/bin/sh
#
# Ce script teste la méthode POST
#
echo Content-type: text/plain
echo
echo REQUEST_METHOD = $REQUEST_METHOD
echo CONTENT_LENGTH = $CONTENT_LENGTH
while read l
do
echo "Couple lu :" $l
done
exit 0
16-4-4. Écriture d'une CGI en Perl▲
Voir cours de Jean-Jacques Dhenin.
16-5. Conclusion - Bibliographie▲
Rien n'est constant, tout change... Et il faut être constamment à l'affût de la nouveauté dans ce domaine très réactif qu'est-celui du « World Wide Web ».
Les quelques références bibliographiques qui suivent illustrent ce cours, mais il est évident qu'elles sont bien insuffisantes pour couvrir tous les aspects que le terme « web » sous-entend !
- RFC 1521 N. Borenstein, N. Freed, « MIME (Multipurpose Internet Mail Extensions) Part One : Mechanisms for Specifying and Describing the Format of Internet Message Bodies, 09/23/1993. (Pages=81) (Format=. txt, .ps) (Obsoletes RFC1341) (Updated by RFC1590)
- RFC 1590 J. Postel, « Media Type Registration Procedure », 03/02/1994. (Pages=7) (Format=.txt) (Updates RFC1521)
- RFC 1630 T. Berners-Lee, « Universal Resource Identifiers in WWW : A Unifying Syntax for the Expression of Names and Addresses of Objects on the Network as used in the World-Wide Web », 06/09/1994. (Pages=28) (Format=.txt)
- RFC 1738 T. Berners-Lee, L. Masinter, M. McCahill, « Uniform Resource Locators (URL) », 12/20/1994. (Pages=25) (Format=.txt)
- RFC 1945 T. Berners-Lee, R. Fielding, H. Nielsen, « Hypertext Transfer Protocol - HTTP/1.0 », 05/17/1996. (Pages=60) (Format=.txt)
- RFC 2068 R. Fielding, J. Gettys, J. Mogul, H. Frystyk, T. Berners- Lee, « Hypertext Transfer Protocol - HTTP/1.1 », 01/03/1997. (Pages=162) (Format=.txt)
- TCP/IP Illustrated Volume 3 W. Richard Stevens - Addison-Wesley - janvier 1996.