Ceci est le dernier d'une série de trois articles traitant de la programmation d'applications réseau en PERL. Cet article ne traite pas à proprement parler de la programmation d'applications réseau mais de l'utilisation de quelques appels systèmes qui sont souvent utilisés dans le code de serveurs. Il part du point où nous nous étions arrêtés dans le précédant . Relisez le si vous avez quelques difficultés avec les concepts utilisés ici.
for ( ;; ) { $remote_host = accept(NEWSOCK, SOCK); die "accept() error: $!\n" unless ($remote_host); # Fait quelque chose close(NEWSOCK); }Malheureusement pendant la phase "Fait quelque chose", le serveur ne peut pas traiter les requêtes arrivant du réseau. Un serveur très chargé peut en recevoir des centaines par seconde. Les traiter séquentiellement n'est pas envisageable. L'idéal serait que le serveur ne fasse qu'exécuter les
accept()
s et que les
traitements spécifiques de chaque requête soient pris en charge en
parallèle par un ou plusieurs autres processus.
Les systèmes d'exploitation du type d'UNIX supportent l'appel
système fork()
pour créer de nouveaux
processus. Lors de l'exécution du fork()
le processus
crée un clone de lui-même - toutes ses structures de
données dont les descripteurs de fichiers (y compris les entrées
sorties bufferisées) sont recopiées dans l'espace du nouveau
processus et tout deux continuent leur exécution à partir de
l'instruction qui suivait le fork()
. La seule différence
entre les deux processus est que l'appel fork()
retourne 0 au
processus cloné (appelé aussi le fils) alors que le processus
original ( appelé aussi le père ) retourne le numéro de
processus du fils.
L'utilisation classique du fork()
dans un serveur consiste
à avoir un parent qui exécute les accept()
et qui
appelle immédiatement fork()
. Le parent boucle sur le
prochain accept()
alors que le fils traite les requêtes de
la machine distante puis se termine quand elle a été
entièrement traitée. Ce qui donne en Perl :
for ( ;; ) { $remote_host = accept(NEWSOCK, SOCK); die "accept() error: $!\n" unless ($remote_host); # Nous sommes dans le contexte du père si fork() a retourn;eacute; un status non null last unless (fork()); close(NEWSOCK); } # Nous somme sortis de la boucle, nous sommes dans le contexte du fils. # Fait quelque chose ...
Le processus fils sort de la boucle for ( quand fork a retourné 0 ). Le parent lui ferme le socket qui venait d'être crée pour traiter la requête entrante et retourne au début de la boucle en attente du prochain accept(). Le père peut sans crainte refermer le socket puisqu'une copie de celui-ci est toujours ouverte dans le contexte du processus fils.
Le fils exécute le code de la boucle "for" décrite dans le précédant article (cf: ;login: Vol. 21 No. 5, octobre 1996) - l'integralité du code d'un mini serveur WEB est donné en fin de cet article. Lorsque le requête est traitée par le fils il peut se terminer après avoir notifié son père de sa fin prochaine. Le père doit acquitter cette notification faute de quoi le processus fils continue de "vivre" dans les limbes jusqu'à le père se termine (de tels processus fils sont connus sous le nom de "zombis").
Sous UNIX la notification au parent est assurée par l'utilisation de
signaux. En particulier à la mort d'un enfant, le signal SIGCHLD (ndtr:
signal enfant) est émis vers le parent qui doit y répondre d'une
façon ou d'une autre. Perl utilise une variable particulière %SIG
pour définir comment un processus doit répondre à
l'arrivée d'un signal : les clefs du tableau associatif
%SIG
sont les noms des signaux, les données du tableau sont
les noms de la routines à exécuter quand le signal est
reçu.
La façon la plus simple de traiter un signal est de l'ignorer :
$SIG{"CHLD"} = "IGNORE";placé au début du programme ceci provoque la mort silencieuse de tous les fils à venir.
if (open(FILE, "< $docroot$path")) { @lines = <FILE>; print NEWSOCK @lines; close(FILE); }où
$docroot
était définie au début du
script et $path
était le chemin du document dans la
requête http. Nous avions noté que le correspondant pouvait
demander
../../../../../../../etc/passwdet ainsi récupérer une copie de
/etc/passwd
. Idéalement un serveur ne devrait
pouvoir accéder qu'à des fichiers situés en
dessous de $docroot
.
La majorité des système du type UNIX supportent l'appel
système chroot()
qui restreint les répertoires auxquels un
processus a accès. Cet appel système prend le nom d'un
répertoire en argument et fait que pour ce processus la racine effective
du file système est ce répertoire. Si notre Web server fait un
chroot($docroot);et reçoit la requête précédante, le correspondant ne recevra que
$docroot/etc/passwd
. L'utilisation de
chroot()
pose cependant un certain nombre de problème. Tout
d'abord seul un processus super utilisateur peut invoquer
chroot()
. Bien que le travail dans un environnement
"chrooté" soit très sécurisé, exécuter l'ensemble
des serveurs réseau dans ce contexte ne l'est pas. Le contournement
usuel consiste à faire quiter le mode super utilisateur au processus
dès qu'il a terminé le chroot()
-
généralement en devenant un utilisateur sans privilège
spécial comme "nobody" par exemple. Ceci s'accomplit simplement en Perl
par :
$user = "nobody"; unless ($uid = (getpwnam($user))[2]) { die "Tentative de lancer le serveur en tant qu'utilisateur inexistant ou supperutilisateur\n"; } # [...] des choses sont faite ici [...] # chroot() avec docroot puis change l'UID effectif. # chroot($docroot) || die "chroot() echec: $!\n"; $> = $uid;La variable spéciale
$>
est l'identifiant effectif du
processus. Les programmes Perl s'exécutant en temps que super utilisateur
peuvent changer cette variable pour changer le niveau de leurs
privilèges.
Le second problème qui survient, une fois que chroot()
a
été exécutée est que le processus n'a plus
accès aux fichiers de configuration du système ou aux fichiers
devices puisqu'ils se trouvent au delà de la racine effective de son
file système. C'est pour cette raison que les serveurs FTP anonymes
nécessitent la duplication de certains fichiers système dans la
zône du FTP anonyme car le serveur FTP exécute un
chroot()
avant de donner l'accès à un utilisateur
anonyme.
Ceci n'est pas un gros problème pour notre serveur Web, puisque tous les
fichiers dont il a besoin se trouvent sous $docroot
. Il a
cependant besoin de connaître le nom du noeud du correspondant pour des
questions d'audit, cette étape doit être accomplie avant que
d'appeler chroot()
.
$user = "nobody"; unless ($uid = (getpwnam($user))[2]) { die "Tentative de lancer le serveur en tant qu'utilisateur innexistant ou supperutilisateur\n"; } # [...] des choses sont faite ici [...] # Résout le nom de la machine distante avant d'invoquer chroot() # $raw_addr = (unpack("S n a4 x8", $remote_host))[2]; $dot_addr = join(".", unpack("C4", $raw_addr)); $name = (gethostbyaddr($raw_addr, AF_INET))[0]; # chroot() avec docroot puis change l'UID effectif. # chroot($docroot) || die "chroot() failed: $!\n"; $> = $uid;Tous les serveur ne peuvent pas être modifié de cette façon pour tourner dans un environnement "chrooté", mais c'est une solution qui doit être envisagée quand on développe un serveur.
syslog()
au lieu de print()
pour afficher les
messages d'erreur et les informations informelles. En général il
n'y pas d'application écoutant la sortie standard d'un démon
réseau, l'utilisation de syslog
est donc un
mécanisme plus approprié que print()
. Voire à cet
effet Syslog.pm
dans le répertoire Perl lib
pour plus de détails.
#!/usr/local/bin/perl use Socket; $docroot = "/home/hal/public_html"; $this_host = "my-server.netmarket.com"; $port = 80; $user = "nobody"; # Parent indigne. Ignore la mort de ses enfants # $SIG{"CHLD"} = "IGNORE"; # Récupere l'UID de l'utilisateur. Avorte si l'UID=0 (Super utilisateur) ou # n'existe pas. # unless ($uid = (getpwnam($user))[2]) { die "Tentative de lancer le serveur en tant qu'utilisateur inexistant ou supper utilisateur\n"; # Initialise les structures C # $server_addr = (gethostbyname($this_host))[4]; $server_struct = pack("S n a4 x8", AF_INET, $port, $server_addr); # Crée le socket # $proto = (getprotobyname("tcp"))[2]; socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "Impossible d'initialiser le socket:$!\n"; # Connecte l'adresse et le port et établit la file d'attente # setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, 1) || die "setsockopt() a échoué $!\n"; bind(SOCK, $server_struct) || die "bind() a echoue: $!\n"; listen(SOCK, SOMAXCONN) || die "listen() a echoue: $!\n"; # Traite les requêtes # for ( ;; ) { # Collect la prochaine requete # $remote_host = accept(NEWSOCK, SOCK); die "accept() error: $!\n" unless ($remote_host); # Nous sommes le père si fork() a retourné un status non nul. # last unless (fork()); close(NEWSOCK); } # Nous sommes sortis de la boucle, nous sommes dans le contexte du fils. # Close master socket # close(SOCK); # Rêsout le nom de la machine distante avant d'invoquer chroot() # $raw_addr = (unpack("S n a4 x8", $remote_host))[2]; $dot_addr = join(".", unpack("C4", $raw_addr)); $name = (gethostbyaddr($raw_addr, AF_INET))[0]; # chroot() avec docroot puis change l'UID effectif. # chroot($docroot) || die "chroot() a echoue: $!\n"; $> = $uid; # Lit la requête du client et récupère $path # while (<NEWSOCK>) { last if (/^\s*$/); next unless (/^GET /); $path = (split(/\s+/))[1]; } # Laisse une ligne d'information sur STDOUT # print "$dot_addr\t$name\t$path\n"; # Répond avec une information ou un message d'erreur # if (open(FILE, "< $path")) { @lines = <FILE>; print NEWSOCK @lines; close(FILE); } else { print NEWSOCK <<"EOErrMsg"; <TITLE>Error</TITLE><H1>Error</H1> Cette erreur est survenue en essayant de charger votre information: $! EOErrMsg } # Fin du travail # close(NEWSOCK);
Traduction de ;login: Vol. 22 No. 5, February 1997.
Dernière édition: 15 mars 1998 phb Original 2/12/97jd |
|