La programmation en Perl : Les trucs du réseau
(Troisième partie)

par Hal Pomeranz, traduction Philippe Bereski.

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.

Faire plusieurs choses en même temps.

Le précédant article présentait la boucle classique utilisée dans un serveur pour traiter des connections réseau.

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.

La protection des données

Dans l'article précédant, notre serveur répondait aux requêtes par :
if (open(FILE, "< $docroot$path")) { 	 
	@lines = <FILE>; 	 
	print NEWSOCK @lines; 	
	close(FILE); 	
}
$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/passwd
et 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.

Enfin !

La programmation réseau est amusante quand on l'a bien comprise. Ne vous gênez pas pour utiliser cet exemple et l'adapter à vos besoins. Une des améliorations qui pourrait être apportée serait d'utiliser 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
Back to the original
Retour à l'index