Dans notre dernier article, nous avons vu comment programmer un client en écrivant un simple utilitaire qui récupère les pages d'un serveur Web distant. Dans celui-ci, nous allons étudier l'écriture d'un petit serveur réseau. En guise d'application nous écrirons un serveur Web primaire. (L'intégralité du code de ce serveur se trouve à la fin du présent article pour les lecteurs qui trouveraient plus commode de le suivre au fure et à mesure des explications). N'hésitez pas à relire l'article précédant si vous n'avez plus en tête les concepts de base utilisés dans cette présentation.
use Socket; $this_host = `my-server.netmarket.com'; $port = 8080; $server_addr = (gethostbyname($this_host))[4]; $server_struct = pack("S n a4 x8", AF_INET, $port, $server_addr); $proto = (getprotobyname(`tcp'))[2]; socket(SOCK, PF_INET, SOCK_STREAM, $proto)|| die "Failed to initialize socket: $!\n";Dans un premier temps le programme charge le module Perl
Socket.pm
. Le nom de la machine où tourne ce serveur
ainsi que son numéro de port sont donnés dans les 2 lignes
suivantes. Ces informations peuvent très bien être définies sur
la ligne de commande ou dans un fichier de configuration. Le programme
invoque ensuite gethostbyname()
pour obtenir l'adresse IP
du serveur et créer la stucture C qui sera utilisée
ultérieurement. Finalement, il invoque socket()
pour
créer un file handle sur cette sockette.Souvenez vous de l'article précédant, le serveur Web utilisait le port 80 par défaut. Pourquoi donc cet exemple utilise-t-il le port 8080 ? Pour des questions de sécurité, les numéros de port inférieurs à 1024 ne peuvent être utilisés que par des serveurs lancés par root. L'arrière pensée de cette règle était de "garantir" que de tels serveurs (telnet, ftp, gopher, ... ), étaient administrés par des personnes "responsables" et que l'on pouvait donc s'y connecter en toute sécurité. Depuis l'explosion du nombre des stations de travail connectées au réseau, cette garantie n'en est plus une.
Revenons à notre exemple. Le serveur doit maintenant se préparer à recevoir des demandes de connexion sur le port et l'adresse définis :
setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR,1) || die "setsockopt() failed: $!\n"; bind(SOCK, $server_struct) || die "bind() failed: $!\n"; listen(SOCK, SOMAXCONN) || die "listen() failed: $!\n";La fonction
setsockopt()
permet au programme de changer
certains paramètres de la sockette. Nous allons revenir sur le
paramètre SO_REUSEADDR
dans un instant. La fonction
bind()
réalise la connexion entre le file handle
SOCK
et l'adresse et le port donnés au début du
programme. Tant qu'un programme demeure lié à un couple (port,
adresse), aucun autre peut le faire. Ceci est très utile et évite
bien des confusions. Cependant même après que le serveur soit mort,
ce couple (port, adresse) reste inutilisable aussi longtemps que la
machine n'a pas rebouté, même si on réexécute exactement le même
serveur. Ceci peut paraître étrange voir même être gênant. En
positionnant à vrai (1) le bit SO_REUSEADDR
- AVANT
l'appel à bind()
- on autorisera d'autres programmes à
réutiliser cette adresse après la mort du serveur courant. Les codes
SOL_SOCKET
et SO_REUSEADDR
sont des
constantes définies dans Socket.pm
.
La fonction listen()
est certainement mal nommée. Elle
ne fait rien d'autre que spécifier la taille de la file des requêtes
en attente de traitement par le serveur. Pratiquement toutes les
implémentations de la couche sockette limitent cette queue à 5
entrées. Vous avez donc intèrêt à les traiter
rapidement. SOMAXCONN
, encore une constante définie dans
Socket.pm
, est généralement intialisée à 5. Si vous
essayez une valeur plus grande le système d'exploitation limitera de
toutes façon la queue à 5 entrées. Solaris 2.x est, à ma
connaissance, le seul système d'exploitation moderne qui accepte une
valeur supérieure à 5. (Il est amusant de noter que
SOMAXCONN
reste défini à 5 dans les headers du
système).
for (;;) { $remote_host = accept(NEWSOCK, SOCK); die "accept() error: $!\n" unless ($remote_host); # do some work here close(NEWSOCK); }L'appel
accept()
extrait de la file de la sockette
SOCK
la requête suivante. Si il n'y a plus de requête,
accept
attend la prochaine. Une nouvelle sockette est
crée, elle sera le nouveau point de communication entre la machine et
son client distant. Les données écrites dans NEWSOCK
seront envoyées à ce client, de même que les données lues dans
NEWSOCK
proviennent de ce client. Pensez toujours à
refermer vos sockettes quand vous n'en avez plus besoin.
La fonction accept
rend une structure C contenant
l'adresse de la machine distante, à moins que l'appel ait échoué, dans
ce cas accept()
retourne undef
. Cette structure
correspond à celle passée à connect
. Vous pouvez
l'utiliser pour récupérer l'adresse de la machine distante comme ceci :
$raw_addr = (unpack("S n a4 x8",$remote_host))[2]; @octets = unpack("C4", $raw_addr); $address = join(".", @octets);Vous pouvez également récupérer le nom de la machine distante par
gethostbyname()
:
$hostname = (gethostbyaddr($raw_addr,AF_INET))[0];Ceci est utile pour tracer les processus. Notez l'utilisation une fois encore à
AF_INET; gethostbyaddr()
à besoin qu'on lui
indique le type de l'adresse qu'on lui passe.
HTTP est un protocole incroyablement simple. Les requêtes envoyées par le butineur sont des lignes de texte ASCII terminées par une ligne vide. Une fois qu'il a rencontré cette ligne vide, le serveur envoit sa réponse au client et rompt la communication. Bien que le client envoye généralement une foule d'informations avec sa requête, en pratique le serveur peut les ignorer toutes à l'exception de celles qui ont la forme suivante :
GET /some/path/to/file.html ...Voici un exemple de code qui extrait de la requête du client le chemin à l'information demandée :
while (<NEWSOCK>) { last if (/^\s*$/); next unless (/^GET /); $path = (split(/\s+/))[1]; }Le serveur se doit maintenant de répondre. En général, le chemin est relatif à la racine d'un répertoire où se trouvent les informations gérées par le serveur - la
$docroot
dans
le jargon du Web. Elle peut-être définie dans un fichier
config
ou sur la ligne de commande. En supposant que
$docroot
a été définie d'une façon ou une autre, on
peut simplement écrire :
if (open(FILE, "< $docroot$path")) { @lines = <FILE>; print NEWSOCK @lines; close(FILE); } else { print NEWSOCK <<"EOErrMsg"; <TITLE>Error</TITLE><H1>Error</H1> The following error occurred while trying to retrieve your information: $! EOErrMsg }Si le fichier requis est accessible, le serveur le recopie simplement dans le file handle
NEWSOCK
. Notez qu'en cas de problème
sur open()
, le serveur envoie un message
d'erreur. Noubliez jamais que vous avez un correspondant à l'autre
bout de la sockette et qu'il ou elle s'attend à recevoir une réponse
à sa requête. Bravo. Si vous rassemblez les divers éléments de code que nous venons d'examiner, vous disposez du squelette d'un serveur Web. L'intégralité du code est donné à la fin de cet article pour vous permettre de mieux revoir tous les concepts que nous venons d'explorer.
/../../../../../../../etc/passwdet je récupèrerai votre fichier des mots de passe. Il est clair que vous aurez besoin d'un contrôle d'accès.
Dans le troisième et dernier article de cette série, nous verrons comment résoudre, entre autre, ces 2 problèmes de notre mini serveur Web.
#!/packages/misc/bin/perl use Socket; $docroot = `/home/hal/public_html'; $this_host = `my-server.netmarket.com'; $port = 8080; # Initialize C structure $server_addr =(gethostbyname($this_host))[4]; $server_struct = pack("S n a4 x8", AF_INET,$port, $server_addr); # Set up socket $proto = (getprotobyname(`tcp'))[2]; socket(SOCK, PF_INET, SOCK_STREAM,$proto)|| die "Failed to initialize socket:$!\n"; # Bind to address/port and set up pending queue setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, 1) || die "setsockopt() failed: $!\n"; bind(SOCK, $server_struct) || die "bind() failed: $!\n"; listen(SOCK, SOMAXCONN) || die "listen() failed: $!\n"; # Deal with requests for (;;) { # Grab next pending request # $remote_host = accept(NEWSOCK, SOCK); die "accept() error: $!\n" unless ($remote_host); # Read client request and get $path while (<NEWSOCK>) { last if (/^\s*$/); next unless (/^GET /); $path = (split(/\s+/))[1]; } # Print a line of logging info to STDOUT $raw_addr = (unpack("S n a4 x8", $remote_host))[2]; $dot_addr = join(".", unpack("C4", $raw_addr)); $name = (gethostbyaddr($raw_addr, AF_INET))[0]; print "$dot_addr\t$name\t$path\n"; # Respond with info or error message if (open(FILE, "< $docroot$path")) { @lines = <FILE>; print NEWSOCK @lines; close(FILE); } else { print NEWSOCK <<"EOErrMsg"; <TITLE>Error</TITLE><H1>Error</H1> The following error occurred while trying to retrieve your information: $! EOErrMsg } # All done close(NEWSOCK); }Traduction de ;login: Vol. 21 No. 5, Octobre 1996.
Dernière édition: 15 mars 1998 phb Original 12/5/96 |
|