CARLIER Julien Année 2005-2006 PATRIE Thomas P. KADIONIK Groupe GA 26 Projet Réseau Programmation d un mini serveur WEB
Sommaire... 1 Introduction... 2 2 Cahier des charges... 3 2.1 Les taches réalisées par notre serveur... 3 2.2 Fonctionnement général d un serveur... 3 2.2.1 Modèle client/serveur... 3 2.2.2 Notion de socket... 4 2.3 Mode de connexion de notre serveur : le mode TCP... 5 3 Fonctionnement du serveur HTTP.... 6 3.1 La boucle principale... 6 3.2 Détail de l initialisation de la connexion... 7 3.3 Extraction du fichier demandé de la requête HTTP du client...7 3.4 Traitement des demandes client... 8 3.4.1 Détermination du type de demande... 9 3.4.2 Traitement de la demande... 11 3.5 Améliorations du serveur... 13 4 Code source de programmation du serveur en langage C...14 5 Conclusion... 18 1
1 Introduction Le réseau informatique est un ensemble de machines et d'équipement reliés entre eux qui servent à acheminer des informations. Il existe différents types de services qui communiquent via un système de réseaux : par exemple le transfert de données via Internet, les communications téléphoniques, ou encore la diffusion d images. L enseignement reçu au cours de ce semestre concerne essentiellement les réseaux de transfert de données. Le projet ici présenté finalise une série de séances de travaux pratiques évolutifs permettant pas à pas de maîtriser la programmation réseaux grâce aux sockets en langage C. L apprentissage résidait en une analyse et une compréhension de programmes basiques en vue de modifications et d améliorations nous amenant finalement à la programmation d un mini serveur web qui renvoie simplement le client vers une page d accueil html. Le projet consiste en la finalisation de ce serveur HTTP en lui ajoutant des fonctionnalités plus poussées. Ainsi selon la requête du client, le serveur doit être capable de renvoyer une page html adaptée, mais aussi de gérer les images potentiellement présentes sur ces pages et les renvoyer également au client, ainsi que d exécuter des scripts ou exécutables placés dans un répertoire particulier. Dans un premier temps nous allons voir ce que nous impose le cahier des charges, et définir les notions qu il est nécessaire de connaître en vue de satisfaire celui-ci. Puis nous décrirons pas à pas le fonctionnement de notre serveur avant de présenter le code du programme en langage C réalisé. 2
2 Cahier des charges 2.1Les taches réalisées par notre serveur Nous devons programmer un serveur web répondant à certains critères au niveau de son mode de fonctionnement et des taches qu il doit effectuer. Un serveur web est un logiciel permettant à des personnes d accéder à des pages web (fichiers au format html) depuis un navigateur installé sur un ordinateur distant. Notre mini serveur doit donc traiter les requêtes provenant des «clients» de type navigateur (Mozilla, Netscape, Internet Explorer ). Ces requêtes ont la forme suivante : http://@ip:numero_port/rep1/rep2/page.html Lors d une requête, si le client ne demande aucune page spécifique, notre serveur doit systématiquement lui renvoyer une page d accueil. Dans le cas contraire, le serveur doit être capable de trouver la page demandée et de la renvoyer vers le client. Si le fichier n existe pas le mini serveur doit renvoyer la page HTTP «404 Not Found» gérée par le navigateur client. Par ailleurs, le serveur doit également gérer les images pouvant être référencées dans une page html appelée par le navigateur client (même si souvent le navigateur gère automatiquement cette tache). Enfin, le mini serveur doit être capable de gérer l exécution de scripts ou d exécutables qui sont placés dans un répertoire appelé cgi-bin/. Pour illustrer le fonctionnement de cette tache on place dans ce répertoire un script printenv dont le code source est : #!/bin/bash Set Depuis le navigateur, la requête http//@ip:numero_port/cgi-bin/printenv doit provoquer l exécution du script printenv, et renvoie le résultat de l exécution vers le navigateur. 2.2Fonctionnement général d un serveur 2.2.1Modèle client/serveur Un serveur est un logiciel capable d'interpréter les requêtes pour un protocole donné, arrivant sur le port associé au protocole et de fournir une réponse avec ce même protocole. Le fonctionnement suit le model «client/serveur», le client étant une application par laquelle peut être demandée la requête (navigateur, client FTP, client de messagerie ). Notre serveur utilisant le protocole HTTP, le client est ici un navigateur, et la requête écrite par l utilisateur est l url de la page souhaitée. La communication entre le navigateur et le serveur se fait en deux temps: Le navigateur effectue une requête HTTP Le serveur traite la requête puis envoie une réponse HTTP 3
En réalité la communication s'effectue en plus de temps si on considère le traitement de la requête par le serveur. 2.2.2Notion de socket De nombreux programmes peuvent être exécutés simultanément sur Internet (Navigateur web, téléchargement de fichiers via FTP). Chacun de ces programmes travaille avec un protocole, toutefois l'ordinateur doit pouvoir distinguer les différentes sources de données. Ainsi chacune de ces applications se voit attribuer une adresse unique sur la machine: un port (la combinaison [adresse IP + port] est alors une adresse unique au monde, elle est appelée socket). L'adresse IP sert donc à identifier de façon unique un ordinateur sur le réseau tandis que le numéro de port indique l'application à laquelle les données sont destinées. De cette manière, lorsque l'ordinateur reçoit des informations destinées à un port, les données sont envoyées vers l'application correspondante. Voici le diagramme de fonctionnement d un socket qui illustre les étapes de fonctionnement d un serveur lors d une connexion : 4
2.3Mode de connexion de notre serveur : le mode TCP Notre serveur utilisera une connexion TCP pour dialoguer avec le client. TCP (Transmission Control Protocol) est un protocole par lequel la transmission se fait en mode connecté (contrairement au protocole UDP), c'est-à-dire qu'il permet aux deux machines qui communiquent de contrôler l'état de la transmission. C est un des protocoles de la couche transport du modèle TCP/IP, représente d'une certaine façon l'ensemble des règles de communication sur internet. 5
3 Fonctionnement du serveur HTTP. Dans cette partie nous allons décrire la démarche adoptée pour faire notre programme et illustrer le raisonnement soit par des diagrammes, soit par des algorithmes. 3.1La boucle principale Notre serveur va suivre le schéma d une connexion TCP selon le diagramme suivant : Récupération du numéro de port Création de la famille d adresse (AF_INET ici) Initialisation du numéro de port Création de la socket TCP (type SOCK_STREAM) Bind sur la socket Mise en attente de requêtes Gestion de l envoi des fichiers Fermeture de la connexion 6
3.2Détail de l initialisation de la connexion Vérification du nombre d arguments de la ligne de commande Dans la console, l utilisateur doit taper deux arguments : le nom du fichier à exécuter, et le numéro de port sur lequel sera effectuée la connexion. A ce stade, le programme vérifie la conformité des arguments et renvoi un message d erreur et un rappel de la syntaxe d une requête. Création de la famille d adresses Il existe plusieurs familles d adresses, chacune correspondant à un protocole particulier, ici AF_INET correspond au protocole internet. Récupération et initialisation du numéro de port On converti la chaine de caractères contenant des chiffres, issue de la console, en l entier correspondant. On passe ensuite ce numéro de port en argument du socket d entrée du serveur. BIND sur le socket Il permet de préciser le type de communication associé au socket (TCP ou UDP par exemple). Mise en attente du serveur Après le bind, l appel système «listen» met le serveur en attente de requêtes et permet aussi d établir une file d attente lorsque le serveur effectuera ensuite l appel «accept» qui marque le début du traitement de la ou des requêtes. Toutes les fonctions qui permettent l établissement de la connexion font partie des bibliothèques standard socket.h, netinet/in.h et netdb.h. Hormis la programmation de la gestion des fichiers, tout ce qui est inclus dans la fonction «main» avait déjà été vu dans les exemples de TP (pingserveur, lotoserveur ). Il nous reste donc à voir comment implémenter les fonctions nécessaires à la gestion de l envoi des pages HTML et des fichiers scripts et images. 3.3Extraction du fichier demandé de la requête HTTP du client On se place au moment où le serveur est en attente de requêtes. Lorsque par exemple dans un navigateur on saisie l adresse suivante : http://@ip:numero_port/rep1/rep2/page.html Le serveur reçoit une requête HTTP dont la première ligne est de la forme : GET /rep1/rep2/page.html http/1.0 La fonction que l on désire ici créer doit retourner seulement l adresse du fichier qui est dans ce cas : rep1/rep2/page.html 7
On appellera buf le tableau contenant la chaîne de caractères lue par le serveur, et adress le tableau contenant l adresse du fichier désiré. Voici l algorithme de la fonction adresse qui renvoie l adresse du fichier demandé : Tableau adresse (tableau buffer) ->tableau adresse_fichier Entier i=0 Tant que buffer[i] / i = i + 1 Tant que buffer[i] (espace) Adresse_fichier[i] = buffer [i + 1] i = i + 1 Retourner adresse_fichier Désormais nous disposons de l adresse du fichier demandé, il ne nous reste plus qu à l interpréter afin de renvoyer vers le client le fichier désiré. 3.4Traitement des demandes client Le cahier des charges du serveur nous impose la gestion suivante des requêtes : Si aucune page n est demandée (ou que la demande est un répertoire), on renvoie la page «index.html» contenue dans le répertoire. Si la page n existe pas, on renvoie le code d erreur HTTP «404 Not Found» au navigateur. Sinon, on renvoie la page demandée Organigramme des différents cas à traiter par le serveur : 8
Réception de la requête Test de validité de la commande Si la commande demandée est valide La commande est erronée Commande d un script Commande d un répertoire Commande d un fichier non exécutable Nous allons maintenant définir plus en détail, le traitement des différentes taches une fois que la commande est reconnue comme valide. 3.4.1Détermination du type de demande On doit d'abord déterminer le type de requête, pour savoir comment la traiter. Il faut savoir si le client demander l'affichage d'une page html, d'une image, l'exécution d'un script, ou une page qui n'existe pas (erreur 404). 9
Fonction est_script : si la demande est dans le sous répertoire cgi-bin la requête est une exécution de script int est_script(char* address) if (strncmp(address, "cgi-bin/", 8) == 0) return 1; else return 0; Si la demande n'est pas une exécution de script, c'est une demande de fichier (avec une extension) ou une demande erroné ("404 Not Found"). Fonction extension : renvoie l'extension du fichier demandé char* extension(char* address) char* extension = (char*)malloc(4*sizeof(char)); extension = address + (strlen(address) - 4); if (strncmp(extension, ".", 1) == 0) extension++; return extension; Fonction est_image : teste si le fichier demandé est une image int est_image(char* address) int i; char* extension = extension(address); char* extensions_possibles[7] = "jpg", "jpeg", "jpe", "bmp", "png", "gif", "tiff"; extension = address + strlen(address) - 4; for (i=0;i<=6;i++) if (strcmp(extension, extensions_possibles[i])==0) return 1; return 0; Fonction est_404 : permet de tester si le fichier demandé est introuvable 10
int est_404(char* address) int fo = open(address, O_RDONLY); if( (fo <= 0) && (strcmp(address, "\0")!= 0) && (strncmp(address, "cgi-bin/", 8)!= 0) ) return 1; return 0; Fonction est_requete_classique : teste si le fichier demandé est une demande de page html (si ce n'est ni un script, ni une image, ni un demande erronée) int est_requete_classique(char* address) if(est_image(address) est_script(address) est_404(address)) return 0; return 1; 3.4.2Traitement de la demande Le traitement de la demande client consiste à afficher soit la page demandée, soit le résultat de l'exécution du script, soit le message d'erreur. 11
La fonction affiche_page permet d'afficher la page html demandée par le client int affiche_page(char* address, int newsd) int fo, n; char buf[200]; if( est_404(address) ) return 0; else fo = open(address, O_RDONLY); while( (n=read(fo, &buf, 200)) > 0 ) write(newsd, &buf, n); return 1; Afin de traiter les différents cas qui se présentent au serveur on utilise le script ci-dessous : if(est_requete_classique(address) (strcmp(address, "cgibin/")==0)) if( (strcmp(temp, "/\0") == 0 ) ) /* On est a la racine ou dans un des sous-repertoires */ address = strcat(address, "index.html"); write(1, "HTTP/1.0 200 OK\nOuverture index.html\n", strlen("http/1.0 200 OK\nOuverture index.html\n")); affiche_page(address, newsd); Ici on regarde la terminaison du chemin demandé par le client. Sinon, si le chemin demandé ne se termine pas par un "/" ou un "\" on rentre dans le cas suivant : else /* On est pas a la racine => On doit ouvrir un fichier */ write(1, "HTTP/1.0 200 OK\nContent-type: text/html\n", strlen("http/1.0 200 OK\nContent-type: text/html\n")); affiche_page(address, newsd); 12
Là on affiche la page html classique sinon 3 cas se présentent au serveur : Le client a demandé l'exécution d'un script Le client a demandé une image Ou le client a émit une demande erronée else /* Le client a demande soit une image soit un script a executer en ligne soit un fichier qui n'existe pas! */ if ((est_script(address) == 1)) write(1, "HTTP/1.0 200 OK\nExecution d'un script en ligne\n", strlen("http/1.0 200 OK\nExecution d'un script en ligne\n")); execution_script(address, newsd); if (est_image(address) == 1) write(1, "HTTP/1.0 200 OK\nContent-type: image/\n", strlen("http/1.0 200 OK\nContent-type: image/")); write(1, extension(address), strlen(extension(address))); write(1, "\n", strlen("\n")); affiche_page(address, newsd); if (est_404(address) == 1) write(1, "HTTP/1.0 200 OK\n404 Not Found\n", strlen("http/1.0 200 OK\n404 Not Found\n")); affiche_page("404.html", newsd); 3.5Améliorations du serveur Dans le cas ou la requête amène à un répertoire, on a fait en sorte que le programme ouvre la page index.html du répertoire si elle existe. 13
4 Code source de programmation du serveur en langage C /********************************************************* ** Nom du fichier source : www.c ** ** Serveur www pour le protocole http ** ** Auteurs : CARLIER/PATRIE ** ** Version et Date : 2.1 13/12/2005 ** ** Serveur web permettant l'affichage des pages html ** ** avec gestion des images, des sous répertoires et ** ** l'execution de scripts. ** **********************************************************/ /*---------------------------------Include-------------------------------*/ #include <string.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <sys/types.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #define DEBUT 4 char* get_address(char* requete, int length) char* filename=(char*)malloc(256*sizeof(char)); char* fin; int i = DEBUT; if(strncmp("get ", requete, DEBUT) == 1) /* strcmp renvoit 1 si les chaines sont differentes */ printf("erreur dans la requete!\n"); fin=strchr(&requete[debut], ' '); /* &requete[debut] pointe sur tout ce qu'il y a après le "GET " */ /* => fin pointe sur le 1er espace du nom du fichier a ouvrir */ while( &requete[i]!= fin ) filename[i-debut]=requete[i]; i++; return filename+1; /* Test si le fichier demande est une image */ int est_image(char* address) int i; char* extension = (char*)malloc(4*sizeof(char)); char* extensions_possibles[7] = "jpg", "jpeg", "jpe", "bmp", "png", "gif", "tiff"; extension = address + strlen(address) - 4; if (strncmp(extension, ".", 1) == 0) /* On a une extension a 3 lettres */ 14
extension++; for (i=0;i<=6;i++) if (strcmp(extension, extensions_possibles[i])==0) return 1; return 0; /* Renvoie l'extension du fichier */ char* extension(char* address) char* extension = (char*)malloc(4*sizeof(char)); extension = address + (strlen(address) - 4); if (strncmp(extension, ".", 1) == 0) extension++; return extension; /* Test si le fichier demande est un script */ int est_script(char* address) if (strncmp(address, "cgi-bin/", 8) == 0) return 1; else return 0; /* Execute un script */ void execution_script(char* address, int newsd) char* commande = (char*)malloc(256*sizeof(char)); commande = address+8; commande = strcat(commande, " 1> cgi-bin/result.txt"); system(commande); affiche_page("cgi-bin/result.txt", newsd); /* Test si le fichier demande est introuvable */ int est_404(char* address) int fo = open(address, O_RDONLY); if( (fo <= 0) && (strcmp(address, "\0")!= 0) && (strncmp(address, "cgi-bin/", 8)!= 0) ) return 1; return 0; /* Test si la requete est une demande de page html existante */ int est_requete_classique(char* address) if( est_image(address) est_script(address) est_404(address) ) return 0; return 1; /* Affiche une page html classique */ 15
int affiche_page(char* address, int newsd) int fo, n; char buf[200]; if( est_404(address) ) return 0; else fo = open(address, O_RDONLY); while( (n=read(fo, &buf, 200)) > 0 ) write(newsd, &buf, n); return 1; main(argc,argv) int argc ; char* argv[] ; /*---------------------------Infos sur le serveur------------------------*/ int sd; /* Identite de la socket sortante */ struct sockaddr_in sa; /* Structure Internet sockaddr_in de la connection sortante */ struct hostent * hptr ; /* Infos sur le serveur */ int port; /* Numero de port du serveur */ /*---------------------------Infos sur le client-------------------------*/ int newsd; /* Identite de la socket entrante */ struct sockaddr_in newsa; /* Structure Internet sockaddr_in de la connection entrante */ int newsalength; struct hostent *newhptr; /* Infos sur le client suivant /etc/hosts */ /*-----------------------------------------------------------------------*/ char buf[256]; /* Buffer sur lequel vont s'echanger les commandes entre client et serveur */ char* address=(char*)malloc(256*sizeof(char)); char* temp=(char*)malloc(256*sizeof(char)); int i, n; int k; /*-------Verification du nombre d'arguments de la ligne de commande------*/ if (argc!= 2) printf("www. Erreur d'arguments\n"); printf("syntaxe : %%./www numero_port\n"); exit(1); /*----------------------Recuperation du numero port----------------------*/ port = atoi(argv[1]); /*------Initialisation de la structure sa avec les infos du serveur------*/ 16
/* Famille d'adresse : AF_INET = PF_INET */ sa.sin_family = AF_INET; /* Initialisation du numero du port */ sa.sin_port = htons(port); sa.sin_addr.s_addr = INADDR_ANY; /*--------------------Creation de la socket TCP-------------------------*/ if((sd = socket(af_inet, SOCK_STREAM, 0)) < 0) /* sd est le numero de la socket sortante. sd <0 signifie que l'ouverture de la socket a echoue */ printf("probleme lors de la creation de socket\n"); exit(1); /* Bind sur la socket => pas de conflit avec les ports deja utilises */ if(bind(sd, (struct sockaddr *)&sa, sizeof(sa)) == -1) printf("probleme avec le bind\n"); exit(1); /*--------Initialisation de la queue d'ecoute des requetes (5 max)------*/ listen(sd, 5); printf("\n\nserveur www en ecoute...\n"); i = 0; /* Compte le nombre de connections entre client et serveur (on le limite a 15)*/ while(1) newsalength = sizeof(newsa) ; /* Si il y a une connection alors on cree une nouvelle structure 'sa' (newsa) derivee de 'sa' pour dialoguer */ if((newsd = accept(sd, ( struct sockaddr* ) &newsa, &newsalength)) < 0 ) printf("erreur sur accept\n"); exit(1); /* Compteur du nombre de connexions */ i++; /* Affiche les infos sur le client */ printf("\n\nconnection No %d sur le port %d...\n", i, ntohs(newsa.sin_port)); n=read(newsd, buf, sizeof(buf)); /* Requete du client a traiter */ address = get_address(buf, n); temp = address + strlen(address)-1; printf("address=%s\n", address); if(est_requete_classique(address) (strcmp(address, "cgi-bin/")==0)) if( (strcmp(temp, "/\0") == 0 ) ) /* On est a la racine ou dans un des sous-repertoires */ address = strcat(address, "index.html"); write(1, "HTTP/1.0 200 OK\nOuverture index.html\n", strlen("http/1.0 200 OK\nOuverture index.html\n")); affiche_page(address, newsd); else /* On est pas a la racine => On doit ouvrir un fichier */ write(1, "HTTP/1.0 200 OK\nContent-type: text/html\n", strlen("http/1.0 200 OK\nContent-type: text/html\n")); 17
affiche_page(address, newsd); else /* Le client a demande soit une image soit un script a executer en ligne soit un fichier qui n'existe pas! */ if ((est_script(address) == 1)) write(1, "HTTP/1.0 200 OK\nExecution d'un script en ligne\n", strlen("http/1.0 200 OK\nExecution d'un script en ligne\n")); execution_script(address, newsd); if (est_image(address) == 1) write(1, "HTTP/1.0 200 OK\nContent-type: image/\n", strlen("http/1.0 200 OK\nContent-type: image/")); write(1, extension(address), strlen(extension(address))); write(1, "\n", strlen("\n")); affiche_page(address, newsd); if (est_404(address) == 1) write(1, "HTTP/1.0 200 OK\n404 Not Found\n", strlen("http/1.0 200 OK\n404 Not Found\n")); affiche_page("404.html", newsd); shutdown(newsd, 2); /* La tache est terminee => on ferme la connection avec le client actuel mais on reste en ecoute (TCP) */ if(i==150) shutdown(sd, 2); printf("fin du serveur. @plouche...\n"); exit(0); /* Fin du "while" */ /* Fermeture du serveur. (Never reached) */ shutdown(sd, 2); printf("fin du serveur. Bye...\n"); exit(0); 5 Conclusion 18
Grâce à quelques notions de base en langage C nous avons pu approfondir notre connaissance de la programmation réseau en réalisant un serveur WEB effectuant les fonction de base, comme l'affichage d'une page html, d'une image ou bien l'exécution d'un script. Malgré une formation essentiellement basée sur l'électronique la connaissance de la programmation étant donnée l'effervescence des réseaux informatiques qui sont des technologies de plus en plus présentes dans les industries et également dans les foyers, comme la domotique. De nombreux appareils électroniques utilisent des réseaux pour communiquer. L installation de réseaux entre ordinateurs ou stations de travail grâce à un équipement de routage ou encore les réseaux locaux ou plus étendus de l Internet ou de la téléphonie sont des exemples d architectures réseaux. Il est donc aussi important pour un ingénieur en électronique que pour un ingénieur en télécommunications d avoir des notions solides sur ces technologies. En tant qu utilisateur très fréquent d Internet, nous avons porté un grand intérêt à ce projet qui nous a permis de mettre en application nos connaissances et de mieux comprendre le fonctionnement d un serveur face à un navigateur. Par ailleurs, la programmation de ce serveur en langage C nous a permis de progresser, par la pratique d une part, et par l utilisation de nouvelles fonctions d autre part dans l utilisation de ce langage. Nous avons finalement programmé un serveur WEB fonctionnel remplissant parfaitement les différents critères imposés par le cahier des charges et renvoyant des pages bien adaptées à la demande du client. 19