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
TP3­Reseau­NomGrooupeEI2­Nom.zip
NomGrooupeEI2 : le nom du groupe EI2 (EI2AD, EI2AG, ou EI-I2B)
Nom1 votre nom si seul,
2. Binôme
TP3­Reseau­NomGrooupeEI2­Nom1­Nom2.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
EI­I2AG : Vendredi 6 juin 2008
EI­I2AD : Vendredi 30 mai 2008
EI­I2B : Vendredi 30 mai 2008
G. BENAY
7
2007/2008