TP 3 Mini client/serveur ftp
Transcription
TP 3 Mini client/serveur ftp
ESCPI TP 3 EI2 TP 3 Mini client/serveur ftp 1 But Le but du TP est de réaliser un client et un serveur de transfert de fichiers à distance. Pour transférer un fichier (par exemple obtenir un fichier depuis une machine distante), un client établit une connexion avec le serveur situé sur cette machine, transmet sa requête, attend l'accusé de réception, puis le contenu du fichier est échangé. La connexion est fermée à la fin du transfert (il ne peut donc y avoir qu'une seule requête par connexion). 2 Un mini-client/serveur ftp Le protocole de communication à implanter est le suivant (il ne s'agit pas du vrai protocole utilisé par ftp) : Serveur ● lancement du serveur (ouverture du port d'écoute) ● attend une demande de connexion ● accepte une demande de connexion ● délègue le traitement de la requête à un fils et se remet en attente de demande de connexion ● le fils : ● lit la requête et l'identifie ● envoie la réponse ● échange les données (si nécessaire). Client ● demande une connexion à un serveur ● construit une requête ● envoie la requête au serveur ● lit la réponse du serveur ● vérifie la réponse ● échange les données (si nécessaire). 2.1 Les requêtes du client Nous spécifions seulement quelques types de requêtes (4) pour simplifier notre cahier des charges : ● obtention d'un fichier (REQUETE_GET), le client demande à récupérer un fichier qui est sur le serveur, ● envoi d'un fichier (REQUETE_PUT), le client demande à déposer un fichier sur le serveur, ● suppression d'un fichier sur le serveur (REQUETE_DEL), le client demande qu'un fichier du serveur soit détruit (la requête seule est envoyée, il n'y a pas de transfert de contenu), ● affichage du contenu d'un répertoire du serveur (REQUETE_DIR), le client demande l'exécution de « ls -la » sur un chemin du serveur 2.2 Les réponses du serveur Après avoir lu la requête, le serveur renvoie un accusé de réception au client. Cet accusé peut être : ● positif (ANSWER_OK), ● négatif (ANSWER_ERROR) ● ou le serveur peut ne pas avoir compris la requête (ANSWER_UNKNOWN). En outre, le client peut ne jamais récupérer d'accusé, à cause d'une coupure de la connexion. Dans le cas d'un accusé négatif, le serveur fournit aussi un code d'erreur permettant d'identifier le problème (par exemple, l'échec peut provenir d'une tentative de récupération d'un fichier inconnu sur le serveur). Pour simplifier, le serveur renvoie la valeur de la variable errno suite à l'action qui a entraîné le rejet de la requête. 2.3 La requête Lors des cas de transfert de fichier (en envoi ou en récupération), il est nécessaire de connaître la taille du fichier transféré pour savoir si, lors d'un read détectant une fin de fichier (dû à la fermeture de la connexion), tout le contenu a bien été récupéré, ou si la connexion a été rompue trop tôt. Une requête est donc de la forme : #define #define #define #define G. BENAY REQUETE_PUT REQUETE_GET REQUETE_DEL REQUETE_DIR 1 2 3 4 1 2007/2008 ESCPI TP 3 struct request { int kind; char path[MAXPATH]; int nbbytes; }; EI2 /* pour PUT seulement */ L'entier kind doit être REQUETE_PUT, REQUETE_GET, REQUETE_DEL ou REQUETE_DIR. La chaîne path contient le nom du fichier à écrire (put), lire (get), détruire (del) ou lister (dir). L'entier nbbytes contient la taille du fichier, lors d'un PUT seulement. 2.4 La réponse Une réponse contient donc l'accusé (ack). Si la réponse est positive (ack vaut ANSWER_OK) et que la requête était un GET, nbbytes contient la taille du fichier que le serveur va envoyer. Si la réponse est négative (ANSWER_ERROR), errnum contient le code de l'erreur (valeur de la variable errno). #define ANSWER_OK 0 #define ANSWER_UNKNOWN 1 #define ANSWER_ERROR 2 struct answer { int ack; int nbbytes; int errnum; }; /* requete inconnue */ /* erreur lors du traitement */ /* pour GET seulement */ /* significatif ssi != 0 et ack == ANSWER_ERROR */ 2.5 Ligne de commande Le serveur miniftpd est démarré sans argument. Le port est prédéfinit et imposé ; on choisira un port libre. L'exécution d'un client peut prendre l'une des formes suivantes : miniftp miniftp miniftp miniftp hostname hostname hostname hostname port port port port get put del dir distfilename localfilename localfilename distfilename distfilename distpathname 2.6 Contrôle d'accès ? Un serveur ftp doit théoriquement vérifier que les fichiers sont accédés avec les droits d'accès du client, et non pas ceux du serveur (d'où une phase initiale d'authentification avec mot de passe). Dans notre cas, le serveur accède aux fichiers sous l'uid de l'utilisateur qui a lancé ce serveur. Le client peut éventuellement être d'un autre uid, mais aucun contrôle n'est effectué. 2.7 Terminaison des processus fils Pour gérer une requête, le serveur « fork » un nouveau processus. Il est intéressant de récupérer le code de retour de ce processus fils, pour pouvoir détecter, par exemple, une terminaison anormale (et cela évite en outre l'apparition de processus zombies). Il existe deux mécanismes de synchronisation avec la terminaison (qui peuvent être combinés) : l'appel système wait attend indéfiniment la terminaison d'un processus fils ; le signal SIGCHLD est envoyé au père lors de la terminaison d'un processus fils. Par défaut, ce signal est ignoré. Rappel : lorsqu'un processus est bloqué par un appel système (par exemple read ou accept) et qu'un signal lui est envoyé, l'appel système échoue et renvoie -1, avec le code d'erreur EINTR (dans la variable errno). • • G. BENAY 2 2007/2008 ESCPI TP 3 EI2 3 Rappels sockets Un socket est un point de communication par lequel le processus peut émettre ou recevoir des informations vers ou en provenance d'un autre socket. Serveur Client Création et attachement d'une socket d'écoute socket() Création cliente Le serveur crée un fils pour traiter les requêtes et lui même se remet à l'écoute Création du service bind() de la socket socket() Le serveur passe en mode écoute il peut accepter des demandes de connexion listen() bind() Demande de connexion accept() connect() fork() read() write() recv() send() read() write() recv() send() close() close() Chaque “close()” ne ferme qu’un seul sens de communication ! 3.1 Fichiers d'en-tête #include #include #include #include #include #include <sys/types.h> <sys/param.h> <sys/socket.h> <netinet/in.h> <arpa/inet.h> <netdb.h> /* /* /* /* constantes, familles... */ struct sockaddr_in */ prototypes pour les fonctions dans inet(3N) */ struct hostent */ 3.2 Le type sockaddr_in Une adresse de socket dans la famille Internet est définie par : struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; /* /* /* /* la famille de protocole */ numero de port */ adresse IP de la machine */ remplissage pour faire 16 octets */ Rappel : L'adresse IP d'une machine (type struct in_addr) est en fait 4 octets, qu'on écrit généralement sous la forme 147.127.64.7. 3.3 Coté client u_long htonl (u_long hostlong); u_short htons (u_short hostshort); struct hostent *gethostbyname (char *name); G. BENAY 3 2007/2008 ESCPI TP 3 EI2 int socket (int domain, int type, int protocol); int connect (int s, struct sockaddr *name, int namelen); On utilise des sockets dans la famille de protocole Internet, de type stream (fiable, fifo, bi-directionnel), créés par : socket (PF_INET, SOCK_STREAM, 0). Pour la connexion (serverhost est le nom de la machine que l'on veut contacter, port est le numéro du port sur cette machine) : { int sc; struct hostent *sp; struct sockaddr_in sins; /* Obtention d'information au sujet de la machine `serverhost' */ sp = gethostbyname (serverhost); if (sp == NULL) { fprintf (stderr, "gethostbyname: %s not found\n", serverhost); exit (1); } /* Creation d'un socket Internet de type stream (fiable, bi-directionnel) */ sc = socket (PF_INET, SOCK_STREAM, 0); if (sc == -1) { perror ("socket failed"); exit (1); } /* Remplissage de la structure `sins' avec la famille de protocoles Internet, * le numero IP de la machine a contacter et le numero de port. */ sins.sin_family = AF_INET; memcpy (&sins.sin_addr, sp->h_addr_list[0], sp->h_length); sins.sin_port = htons (port); /* Tentative d'etablissement de la connexion. */ if (connect (sc, (struct sockaddr *)&sins, sizeof(sins)) == -1) { perror ("connect failed"); exit (1); } } 3.4 Coté serveur int int int int int socket (int domain, int type, int protocol); setsockopt (int s, int level, int optname, char *optval, int optlen); bind (int s, struct sockaddr *name, int namelen); listen (int s, int backlog); accept (int s, struct sockaddr *addr, int *addrlen); Ce qui s'utilise (port est le numéro du port sur lequel écoute le serveur) : { struct sockaddr_in soc_in; int val; int ss; /* socket Internet, de type stream (fiable, bi-directionnel) */ ss = socket (PF_INET, SOCK_STREAM, 0); /* Force la reutilisation de l'adresse si non allouee */ val = 1; setsockopt (ss, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); /* Nomme le socket: socket inet, port PORT, adresse IP quelconque */ soc_in.sin_family = AF_INET; soc_in.sin_addr.s_addr = htonl (INADDR_ANY); soc_in.sin_port = htons (port); G. BENAY 4 2007/2008 ESCPI TP 3 EI2 bind (ss, &soc_in, sizeof(soc_in)); /* Prepare le socket a la reception de connexions */ listen (ss, 5); while (1) { struct sockaddr_in from; int len; int f; /* Accepte une connexion. * Les parametres `from' et `len' peuvent etre NULL. */ len = sizeof (from); f = accept (ss, (struct sockaddr *)&from, &len); } /* ... */ } Attention : il manque le traitement d'erreur, qui est indispensable avec les sockets, du fait de la forte probabilité de défaillance. 4 Rappels Unix Tous les appels systèmes renvoient -1 en cas d'erreur. Dans ce cas, la variable errno contient le code de l'erreur. 4.1 Obtenir des informations sur un fichier : stat Pour obtenir des information sur un fichier, utiliser : ● int stat (char *pathname, struct stat *buf); ● int fstat (int fd, struct stat *buf); La structure struct stat contient de nombreux champs (voir man stat pour plus de détails), dont st_uid (propriétaire du fichier), st_size (taille du fichier), st_mode (droit d'accès), st_mtime (date de dernière modification)... #include <sys/types.h> #include <sys/stat.h> main (int argc, char **argv) { struct stat buf; if (stat (argv[1], &buf) == -1) perror (argv[1]); else printf ("%s: proprietaire %d, taille %d\n", argv[1], buf.st_uid, buf.st_size); } 4.2 Ouverture d'un fichier open Pour obtenir un descripteur de fichier permettant d'accèder (lecteur ou écriture) à un fichier, on utilise l'appel système open. Les trois formes d'utilisation habituelles sont : int fd = open ("toto", O_RDONLY); int fd = open ("toto", O_WRONLY | O_CREAT | O_TRUNC, 0644); int fd = open ("toto", O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) { erreur... } La première ligne ouvre le fichier en lecture ; la deuxième ouvre le fichier pour écriture, avec création s'il n'existe pas et troncature s'il existe déjà ; la troisième ligne ouvre le fichier pour écriture, avec création s'il n'existe pas et erreur s'il existe déjà. Le troisième argument est utilisé pour définir les droits d'accès (ici rw-r--r--) s'il y a création du G. BENAY 5 2007/2008 ESCPI TP 3 EI2 fichier. 4.3 Destruction d'un fichier unlink L'appel système : int unlink (char *pathname); détruit le lien spécifié par pathname. Si ce lien était le dernier lien vers le fichier, le fichier est effacé. 4.4 Lecture/écriture read /write int read (int fd, void *buf, int nbyte); int write (int fd, const void *buf, int nbyte); L'appel système read lit sur le descripteur au plus nbyte octets et les range à l'adresse buf. Il renvoie le nombre d'octets effectivement lus, ou 0 si la « fin de fichier » a été atteinte (il n'y a et il n'y aura plus rien à lire : socket fermé ou fichier complètement lu), ou -1 en cas d'erreur. L'appel système write écrit sur le descripteur au plus nbyte octets rangés à l'adresse buf. Il renvoie le nombre d'octets effectivement écrits, ou -1 en cas d'erreur. Quand read ou write sont appliqués à un fichier, le nombre d'octets lus ou écrits est toujours le nombre demandé (sauf lorsque la fin du fichier est atteinte) ; quand ils sont appliqués à un descripteur associé à un socket, le nombre d'octets lus ou écrits peut être inférieur au nombre demandé. 4.5 Les signaux Un signal est un événement asynchrone auquel il est possible d'associer un traitement spécifique (une procédure qui sera invoquée par le système à la délivrance du signal). En absence de traitement, un signal entraîne en général la mort du processus destinataire. L'association d'un traitement se fait au moyen de la primitive signal : void traitement_sig (int sig) { signal (SIGCHLD, traitement_sig); /* remise en place du traitement */ ... } ... main() { ... signal (SIGCHLD, traitement_sig); ... } 5 Déroulement des tps Fournis dans un paquet zip (miniftp.zip) ou tar (miniftp.tgz): ● requetes.h : les types et macro-définitions utiles ; ● common.h, common.c : deux petites procédures bien utiles, notamment copy_n_bytes ; ● miniftp.c : l'architecture du client ; ● miniftpd.c : l'architecture du serveur ; ● Makefile : pour compiler. 5.1 Le Client écriture du client miniftp. Le code get est déjà fourni, ● compiler et exécuter en utilisant le serveur sur kirov ou karkov (port 38590) ; ● écrire les autres requêtes ; écrire le code des autres requêtes (put, dir, del) ; ● valider l'ensemble. 5.2 Le Serveur écriture du serveur miniftpd. La réponse à la requête get est fournie. ● écrire les autres réponses aux requêtes (put, dir, del) ; ● valider soigneusement. 5.3 Rapport Rappel vous devez fournir un rapport avec l'ensemble des programmes : ● Le serveur, ● Le client, ● Une présentation des fonctions réalisées. G. BENAY 6 2007/2008 ESCPI TP 3 EI2 1. Personne seule TP3ReseauNomGrooupeEI2Nom.zip NomGrooupeEI2 : le nom du groupe EI2 (EI2AD, EI2AG, ou EI-I2B) Nom1 votre nom si seul, 2. Binôme TP3ReseauNomGrooupeEI2Nom1Nom2.zip NomGrooupeEI2 : le nom du groupe EI2 (EI2AD, EI2AG, ou EI-I2B) Nom1 le nom d'un des membres du binôme Nom2 le nom de l'autre membre du binôme Vous envoyez votre rapport par courrier électronique à l'adresse : tard le : [email protected] au plus EII2AG : Vendredi 6 juin 2008 EII2AD : Vendredi 30 mai 2008 EII2B : Vendredi 30 mai 2008 G. BENAY 7 2007/2008