• Aucun résultat trouvé

Exemple complet : le serveur universel

On va maintenant d´efinir une commande server telle que server port cmd arg1 . . . argn

re¸coit les demandes de connexion au num´ero port, et `a chaque connexion lance la commande cmd avec arg1 . . . argn comme arguments, et la connexion comme entr´ee et sortie standard.

Par exemple, si on lance ./server 8500 grep foo

sur la machine pomerol, on peut ensuite faire depuis n’importe quelle machine ./client pomerol 8500 < /etc/passwd

en utilisant la commande client ´ecrite pr´ec´edemment, et il s’affiche la mˆeme chose que si on avait fait

./grep foo < /etc/passwd

sauf que grep est ex´ecut´e sur pomerol, et non pas sur la machine locale.

La commande server constitue une application serveur “universel”, dans la mesure o`u elle regroupe le code d’´etablissement de service qui est commun `a beaucoup de serveurs, tout en d´el´eguant la partie impl´ementation du service et du protocole de communication, propre `a chaque application ou programme lanc´e par server.

1 open Sys;; 2 open Unix;; 3

4 let server () =

5 if Array.length Sys.argv < 2 then begin

6 prerr_endline "Usage: client <port> <command> [arg1 ... argn]"; 7 exit 2;

8 end;

9 let port = int_of_string Sys.argv.(1) in

10 let args = Array.sub Sys.argv 2 (Array.length Sys.argv - 2) in 11 let host = (gethostbyname(gethostname())).h_addr_list.(0) in 12 let addr = ADDR_INET (host, port) in

13 let treat sock (client_sock, client_addr as client) = 14 (* log information *)

15 begin match client_addr with 16 ADDR_INET(caller, _) ->

17 prerr_endline ("Connection from " ^ string_of_inet_addr caller); 18 | ADDR_UNIX _ ->

19 prerr_endline "Connection from the Unix domain (???)";

20 end;

21 (* connection treatment *) 22 let service (s, _) =

23 dup2 s stdin; dup2 s stdout; dup2 s stderr; close s; 24 execvp args.(0) args in

25 Misc.double_fork_treatment sock service client in 26 Misc.tcp_server treat addr;;

27

28 handle_unix_error server ();;

L’adresse fournie `a tcp_server contient l’adresse Internet de la machine qui fait tourner le programme ; la mani`ere habituelle de l’obtenir (ligne 11) est de chercher le nom de la ma- chine (renvoy´e par l’appel gethostname) dans la table /etc/hosts. En fait, il existe en g´en´eral plusieurs adresses pour acc´eder `a une machine. Par exemple, l’adresse de la machine pauillac est 128.93.11.35, mais on peut ´egalement y acc´eder en local (si l’on est d´ej`a sur la machine pauillac) par l’adresse 127.0.0.1. Pour offrir un service sur toutes les adresses d´esignant la machine, on peut utiliser l’adresse inet_addr_any.

Le traitement du service se fera ici par un≪double fork≫apr`es avoir ´emis quelques informa-

tions sur la connexion. Le traitement du service consiste `a rediriger l’entr´ee standard et les deux sorties standard vers la prise sur laquelle est effectu´ee la connexion puis d’ex´ecuter la commande souhait´ee. (Notez ici que le traitement du service ne peut pas se faire de fa¸con s´equentielle.)

Remarque : la fermeture de la connexion se fait sans intervention du programme serveur. Premier cas : le client ferme la connexion dans le sens client vers serveur. La commande lanc´ee par le serveur re¸coit une fin de fichier sur son entr´ee standard. Elle finit ce qu’elle a `a faire, puis appelle exit. Ceci ferme ses sorties standard, qui sont les derniers descripteurs ouverts en ´ecriture sur la connexion. (Le client recevra alors une fin de fichier sur la connexion.) Deuxi`eme cas : le client termine pr´ematur´ement et ferme la connexion dans le sens serveur vers client. Le serveur peut alors recevoir le signal sigpipe en essayant d’envoyer des donn´ees au client, ce qui peut provoquer la mort anticip´ee par signal SIGPIPE de la commande du cˆot´e serveur ; ¸ca convient parfaitement, vu que plus personne n’est l`a pour lire les sorties de cette commande.

Enfin, la commande cˆot´e serveur peut terminer (de son plein gr´e ou par un signal) avant d’avoir re¸cu une fin de fichier. Le client recevra alors un fin de fichier lorsqu’il essayera de lire et un signal SIGPIPE (dans ce cas, le client meurt imm´ediatement) ou une exception EPIPE (si le signal est ignor´e) lorsqu’il essayera d’´ecrire sur la connexion.

Pr´ecautions

L’´ecriture d’un serveur est en g´en´eral plus d´elicate que celle d’un client. Alors que le client connaˆıt le serveur sur lequel il se connecte, le serveur ignore tout de son client. En particulier, pour des services publics, le client peut ˆetre≪hostile≫. Le serveur devra donc se prot´eger contre

tous les cas pathologiques.

Un attaque typique consiste `a ouvrir des connexions puis les laisser ouvertes sans transmettre de requˆete : apr`es avoir accept´e la connexion, le serveur se retrouve donc bloqu´e en attente sur la prise et le restera tant que le client sera connect´e. L’attaquant peut ainsi saturer le service en ouvrant un maximum de connexions. Il est important que le serveur r´eagisse bien `a ce genre d’attaque. D’une part, il devra pr´evoir un nombre limite de connexions en parall`ele et refuser les connexions au del`a afin de ne pas ´epuiser les ressources du syst`eme. D’autre part, il devra interrompre les connexions rest´ees longtemps inactives.

Un serveur s´equentiel qui r´ealise le traitement lui mˆeme et sans le d´el´eguer `a un de ses fils est imm´ediatement expos´e `a cette situation de blocage : le serveur ne r´epond plus alors qu’il n’a rien `a faire. Une solution sur un serveur s´equentiel consiste `a multiplexer les connexions, mais cela peut ˆetre complexe. La solution avec le serveur parall`ele est plus ´el´egante, mais il faudra tout de mˆeme pr´evoir des ≪timeout≫, par exemple en programmant une alarme.