Retrouvez cet article dans : Linux Magazine 103
De nombreux logiciels ont besoin de télécharger des fichiers à distance. Dans ce domaine, l’un des protocoles les plus anciens, mais aussi le plus couramment utilisé, est File Transfer Protocol (FTP). Après un rapide survol de la recommandation, cet article présente le développement d’une API écrite en langage C pour faciliter la mise en œuvre d’un client FTP au sein des logiciels.
1. Survol de la recommandation FTP
1.1 Le modèle client-serveur
Le protocole FTP spécifié dans la RFC959 [1] a été conçu pour :
1. Faciliter le partage de fichiers.
2. Encourager l’utilisation de machines distantes.
3. Rendre un programme indépendant des spécificités de gestion des fichiers sur les systèmes distants.
4. Transférer des données efficacement et de manière sûre.
C’est un modèle client-serveur s’appuyant sur les couches TCP et IP comme indiqué en figure 1.

Figure 1 : Architecture d’un client-serveur FTP
1.2 Les canaux de communication
L’établissement d’une connexion FTP consiste, pour le client, à ouvrir un canal de contrôle. Ensuite, un canal de données est éventuellement ouvert. Le canal de contrôle sert au transfert des commandes, des réponses aux commandes et des messages spontanés. Le canal de données n’est établi que si les commandes engendrent un transfert de données.
La communication sur le canal de contrôle est bidirectionnelle et conforme au protocole telnet décrit dans la RFC854 [2]. Dans la pratique, telnet est utilisé de manière très basique. Le dialogue est alterné dans le sens où le client envoie une commande à laquelle le serveur répond par un ou plusieurs messages. Chaque commande a une liste prédéfinie de réponses possibles. Le serveur peut aussi envoyer des messages spontanés pour fournir des informations diverses telles que " le système va s’arrêter dans 15 minutes " ou " le délai de connexion est expiré ".
La communication sur le canal de données est unidirectionnelle. Le sens de communication dépend du type de commande en exécution.
Un canal est en fait une connexion TCP comme illustré en figure 2.

Figure 2 : Les canaux de communication
1.3 Modes actif et passif
Par défaut, le client demande l’ouverture du canal de contrôle sur le port TCP 21 de la machine serveur. Cela suppose que le serveur FTP est en écoute sur ce port. Les valeurs par défaut des ports pour les deux canaux sont consignées dans le fichier /etc/services :
$ cat /etc/services | grep ftpftp-data 20/tcp ftp 21/tcptftp 69/udp sftp 115/tcp ftps-data 989/tcp # FTP over SSL (data) [...] $
L’établissement du canal de données dépend du mode de fonctionnement du serveur : actif ou passif.
En mode actif, le serveur établit le canal de données sur le port du client qui est, par défaut, le port qu’il a utilisé pour la connexion de contrôle. Le client a la possibilité de notifier au serveur un port de données différent par la commande PORT. En pratique, le mode actif est peu utilisé, car souvent non supporté par les clients. De plus, si la machine sur laquelle le client s’exécute est protégée par un pare-feu, il y a de grandes chances que le serveur ne pourra pas établir de connexion sur le port du client.
En mode passif, le client établit le canal de données en utilisant la commande PASV pour demander au serveur de proposer un numéro de port TCP sur lequel le client établira un canal de données. Ce mode simplifie l’écriture des clients et sera celui utilisé par l’API présentée dans la suite.
1.4 Les commandes
Les commandes sont des lignes de données ASCII au format :
Commande paramètre1 paramètre2... CRLF
La commande est un mot de 4 caractères au maximum. Les majuscules et minuscules peuvent être utilisées de manière indifférente. Les paramètres peuvent ne pas apparaître si la commande n’en nécessite pas ou s’ils sont optionnels.
Les commandes les plus couramment utilisées sont décrites dans le tableau 1 (les paramètres décrits entre crochets sont facultatifs et les commandes marquées d’un astérisque font partie de l’ensemble minimum qu’un serveur FTP doit supporter pour être considéré conforme à la recommandation).

1.5 Les réponses
Une réponse est codée comme suit :
- La première ligne commence par un code à trois digits
xyzimmédiatement suivi d’un tiret éventuellement suivi d’un texte et se termine par les caractèresCRetLF. - Les lignes suivantes contiennent du texte quelconque et sont terminées par les caractères
CRetLF - La dernière ligne commence par le code à trois digits
xyzde la première ligne suivi d’un caractère espace éventuellement suivi d’un texte et se termine par les caractèresCRetLF. Si la réponse est monoligne, elle suit ce codage.
xyz sont 3 digits qui indiquent l’état d’avancement de la commande en cours. Le principe de codage des digits va du général au particulier : le premier digit donne une information qui est explicitée avec les deux digits qui suivent. Le tableau 2 donne les valeurs possibles pour le premier digit. La recommandation spécifie une liste de valeurs possibles pour les digits qui suivent, mais il n’est pas utile de les citer ici d’autant plus que nous verrons dans l’implémentation de l’API qu’il est possible d’implémenter le protocole en ne tenant compte que de la valeur du premier digit.

Le texte qui suit les digits dans les lignes de réponses est facultatif et souvent présent à titre d’information à l’exception de certaines commandes comme PASV qui impliquent une réponse avec un texte formaté. Le texte peut être composé de plusieurs lignes.
Les réponses sont la plupart du temps monolignes. Voici par exemple la réponse d’un serveur quand il demande le mot de passe d’un utilisateur :
331 Password required for foo.
Dans ce cas, le code 3 indique que le serveur a accepté la commande précédente et est en attente d’informations supplémentaires : le mot de passe.
Les réponses multilignes ne sont généralement utilisées que pour afficher les bannières d’accueil lorsque le client se connecte (fichier /etc/ftpwelcome). À titre d’exemple, nous présentons la bannière émise par un serveur FTP tournant sur un système Linux après la phase d’identification du client. Le code suivi d’un tiret est affiché sur les lignes intermédiaires, mais ce n’est pas obligatoire selon la recommandation. On notera surtout le code 230 suivi d’un tiret en première ligne et le même code 230 suivi d’un caractère espace en dernière ligne :
230-Linux toto-host 2.6.22-14-generic #1 SMP Tue Dec 18 08:02:57 UTC 2007 i686 230- 230- The programs included with the Ubuntu system are free software; 230- the exact distribution terms for each program are described in the 230- individual files in /usr/share/doc/*/copyright. 230- 230- Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by 230- applicable law.230User foo logged in.
Dans ce cas, le code 2 indique que la commande précédente a été acceptée et qu’une nouvelle commande peut être lancée.
2. L’API
Après ce rapide passage en revue de la RFC959, il est possible de décrire l’API qui facilite le développement de clients telnet. Dans la description en couche du modèle FTP, l’API est un élément situé juste en dessous du client comme indiqué en figure 3.

Figure 3 : L’API dans le modèle FTP
Le nom de cette API est ROOF (pour " Remote Operations On Files "). Elle se présente sous la forme d’une bibliothèque partagée disponible en open source et hébergée sur Sourceforge [3].
2.1 Installation de ROOF
Téléchargez le fichier .tgz à partir de Sourceforge. Décompressez le fichier avec la commande :
$ tar xvfz roofxxx.tgz
L’arborescence se présente comme indiqué en figure 4.

Figure 4 : L’arborescence de l’API
Le répertoire include contient le fichier roof.h dans lequel sont définis les services et structures de données publiques. Ce fichier devra être inclus par tout utilisateur de la bibliothèque.
Le répertoire lib contient l’implémentation de la bibliothèque libroof.so qui sera liée dynamiquement avec le programme utilisateur. Les fichiers sont roof.c pour l’API et roof_p.h pour les définitions internes.
Le répertoire client contient un exemple de client FTP qui utilise l’API : roof.
Le répertoire man contient les manuels en ligne de la bibliothèque. Le manuel se répartit dans les sections 1 (commande roof pour le client), 3 (API) et 7 (description générale).
Le processus de génération utilise un script appelé roof_install.sh basé sur cmake ([4] et [6]) qu’il faut donc avoir installé sur son système. Ici est montré comment générer et installer à l’aide de cmake dans le répertoire par défaut /usr/local (l’installation requiert les droits du super utilisateur) :
$cd roofxxx$cmake .-- Check for working C compiler: /usr/bin/gcc [...] -- Build files have been written to: [...] $make[...] Linking C executable roof [...] $sudo make install[...] Linking C executable CMakeFiles/CMakeRelink.dir/roof Install the project... [...]
Pour vérifier l’installation, consultez un manuel en ligne de roof (en mettant éventuellement à jour la variable d’environnement MANPATH avec /usr/loca/man) :
$ man 3 roof
ROOF(3) Linux Programmer’s Manual ROOF(3)
NAME
roof - API for Remote Operations On Files
[...]
Il est aussi possible de tester l’exécutable roof (en mettant éventuellement à jour la variable d’environnement LD_LIBRARY_PATH avec /usr/local/lib si la liaison dynamique avec libroof.so pose problème) :
$ roof roof 1.5 [...] Type ‘help’ or ‘?’ for the list of available commands. ftp> quit $
Dans la suite de l’article, sont décrites les parties principales de la bibliothèque pour comprendre dans le détail le passage de la recommandation RFC959 à l’implémentation. Les extraits de code cités ont parfois été élagués pour rendre cet article le plus concis possible. Le lecteur est invité à télécharger les sources complets à partir de Sourceforge pour avoir tous les détails d’implémentation.
2.2 Initialisation
Pour être pratique et robuste, une API se doit d’être réentrante de sorte à supporter plusieurs instances d’exécution en même temps. Cette précaution trouve toute sa signification quand le logiciel qui l’utilise est multithreadé : plusieurs threads s’exécutant en parallèle peuvent utiliser la bibliothèque. Un mutex est donc créé et initialisé dans le point d’entrée de la bibliothèque déclaré comme suit, pour être identifié et exécuté par le linker dynamique au moment du chargement (cf. [5] pour plus de détails) :
void __attribute__ ((constructor)) roof_initialize(void);
void roof_initialize(void)
{
int rc;
// Création du mutex (non vérouillé)
rc = pthread_mutex_init(&roof_mtx, NULL);
[...]
} // roof_initialize
Deux macros sont définies pour verrouiller et déverrouiller le mutex :
#define ROOF_LOCK() (pthread_mutex_lock(&roof_mtx)) #define ROOF_UNLOCK() (pthread_mutex_unlock(&roof_mtx))
2.3 Le contexte
Le contexte est une structure de données qui identifie une instance de la bibliothèque. Il y a un contexte de type roof_ctx_t par utilisateur :
typedef struct
{
void *ctx; // Contexte utilisateur
} roof_ctx_t;
Le champ ctx pointe sur des données privées à l’utilisateur. La bibliothèque n’a aucune action sur ce pointeur. Cela permet simplement à l’utilisateur d’associer des données quelconques à son instance d’exécution dans la bibliothèque. En interne, le contexte utilisateur est stocké dans le champ ctx de la structure roof_context_t définie comme suit dans roof_p.h :
typedef struct
{
unsigned int debug_level; // Niveau de debug
char *iobuf; // Buffer d’E/S
unsigned int l_iobuf; // Taille du buffer d’E/S
void *ctx; // Données utilisateur
int busy; // 1, si contexte occupé
int internal_iobuf; // 1, si buffer d’E/S alloué en interne
int ctrl; // Socket de la cnx de contrôle
unsigned int timeout_ms; // Timeout avec le serveur en ms
char type; // Type pour la commande TYPE
char code; // Code pour la commande TYPE
} roof_context_t;
Si cette structure apparaissait dans le fichier header public roof.h, l’utilisateur de la bibliothèque serait tenté d’utiliser les champs explicitement dans son code. Cela pourrait rendre son programme incompatible avec de futures versions de la bibliothèque (si les champs sont renommés ou disparaissent) et pourrait porter atteinte à l’intégrité de la bibliothèque (si les champs sont modifiés à l’insu de l’API).
Dans la suite, roof_ctx_t sera appelé " contexte externe " (celui vu par l’utilisateur) et roof_context_t sera appelé " contexte interne " (celui vu par la bibliothèque). Pour retrouver le second à partir du premier, la bibliothèque utilise la macro ROOF_CTX() qui retrouve l’adresse du contexte interne à partir de celle du contexte externe et l’offset du champ ctx dans la structure roof_context_t :
#define ROOF_CTX(p) ((roof_context_t *)\
((char *)p - offsetof(roof_context_t, ctx)))
Inversement, le contexte externe est retrouvé à partir du contexte interne grâce à la macro ROOF_EXT_CTX() qui ne fait que retourner l’adresse du champ ctx de roof_context_t :
#define ROOF_EXT_CTX(p) ((roof_ctx_t *)&(p->ctx))
La bibliothèque définit un nombre maximum de contextes avec la constante ROOF_NB_MAX_CTX qui sert à dimensionner la table des contextes roof_context[] dans roof.c.
La toute première chose à faire pour l’utilisateur de la bibliothèque est de réserver un contexte à l’aide du service roof_new() :
roof_ctx_t *roof_new(
unsigned int timeout_ms,// Timeout (ms) pour interactions avec le serveur
// 0 = Attente infinie
char *iobuf, // Buffer d’E/S (Défaut si NULL)
unsigned int l_iobuf, // Longueur du buffer d’E/S (Défaut si 0)
void *ctx // Contexte utilisateur
)
{
unsigned int i;
ROOF_LOCK();
// Recherche d’un contexte libre
for (i = 0; i < ROOF_NB_MAX_CTX; i ++)
{
if (0 == roof_context[i].busy)
{
roof_context[i].busy = 1;
break;
}
} // End for
ROOF_UNLOCK();
if (i >= ROOF_NB_MAX_CTX)
{
errno = ENOSPC;
return NULL;
}
roof_context[i].debug_level = 0;
// Si buffer alloué par l’utilisateur
if (iobuf && l_iobuf)
{
[...]
roof_context[i].iobuf = iobuf;
roof_context[i].l_iobuf = l_iobuf;
roof_context[i].internal_iobuf = 0;
}
else // Buffer alloué en interne
{
roof_context[i].iobuf = (char *)malloc(ROOF_IO_BUF_SIZE);
[...]
roof_context[i].l_iobuf = ROOF_IO_BUF_SIZE;
roof_context[i].internal_iobuf = 1;
}
roof_context[i].ctrl = -1;
roof_context[i].timeout_ms = timeout_ms;
roof_context[i].ctx = ctx;
return (roof_ctx_t *)&(roof_context[i].ctx);
} // roof_new
Un contexte libre (champ busy = 0) est recherché dans la table globale des contextes. La recherche se fait dans une section critique protégée par le mutex (macros ROOF_LOCK() et ROOF_UNLOCK()) pour assurer la réentrance. Le contexte trouvé est initialisé avec les paramètres passés au service. Le premier paramètre timeout_ms est le temps maximum en millisecondes que le service attendra pour recevoir une réponse de la part du serveur. Il est conseillé de le positionner à quelques secondes de sorte à éviter d’avoir un programme qui se met en attente infinie si le serveur ne répond pas. Les paramètres suivants iobuf et l_iobuf sont respectivement l’adresse et la taille du buffer d’entrée/sortie utilisé pour le dialogue avec le serveur. Si l’appelant passe NULL pour l’adresse ou 0 pour la taille, le buffer sera alloué en interne par le service avec une taille par défaut de ROOF_IO_BUF_SIZE octets (défini en interne dans roof_p.h). La valeur retournée par le service est NULL en cas d’erreur ou le contexte externe en cas de succès (dans ce cas, c’est l’adresse du champ ctx de la structure roof_context_t). Du point de vue de l’utilisateur, ce contexte est un identifiant qu’il devra passer à tous les autres services pour identifier son instance d’exécution.
Lorsque l’utilisateur n’a plus besoin de son contexte d’exécution, il fait appel à la fonction roof_delete() pour libérer les ressources :
void roof_delete(roof_ctx_t *pContext) // Contexte externe
{
roof_context_t *pCtx = ROOF_CTX(pContext);
// Désallocation du buffer d’E/S si alloué en interne
if (pCtx->internal_iobuf)
{
free(pCtx->iobuf);
}
// Fermeture de socket de contrôle si ouverte
if (pCtx->ctrl >= 0)
{
shutdown(pCtx->ctrl, SHUT_RDWR);
close(pCtx->ctrl);
}
ROOF_LOCK();
pCtx->busy = 0;
ROOF_UNLOCK();
} // roof_delete
La fonction effectue les opération inverses à roof_new(). Le buffer d’entrée/sortie est libéré s’il a été alloué en interne (champ internal_iobuf != 0), la socket sur le canal de contrôle est fermée " proprement " à l’aide de shutdown(), puis close() si elle est ouverte et enfin le champ busy est mis à 0 pour marquer le contexte comme étant libre pour un nouvel utilisateur.
2.4 Lecture/écriture sur le réseau
La communication avec le serveur utilise le protocole TCP/IP qui est masqué par la bibliothèque socket de Linux. Cela permet d’envoyer et de recevoir des données sur le réseau via un descripteur de fichier en utilisant les appels système classiques read() et write() comme si on écrivait ou lisait un fichier (c’est l’application du fameux concept de base " tout est fichier " d’Unix).
Dans la bibliothèque, deux fonctions roof_read() et roof_write() encapsulent les appels système read() et write() pour essentiellement utiliser la notion de contexte où se trouvent des informations utiles comme le niveau de debug utilisé par certaines macros de trace et traiter le retour erreur EINTR. Un signal, étant asynchrone, peut survenir à tout moment et interrompre les instructions en cours pour déclencher un handler préalablement défini par l’utilisateur pour le gérer. Il s’avère que, sous Linux, la plupart des appels système retournent en erreur avec la variable errno positionnée à EINTR s’ils sont interrompus par un signal. roof_read() et roof_write() testent donc ce code d’erreur pour réitérer l’appel système.
Voici la fonction roof_write() :
static int roof_write(
roof_context_t *pCtx, // Contexte interne
int fd, // Descripteur de sortie
const void *buf, // Buffer d’écriture
size_t len // Nombre d’octets à écrire
)
{
int rc;
unsigned int l;
l = len;
do
{
rc = write(fd, ((const char *)buf) + (len - l), l);
if (rc < 0)
{
if (EINTR == errno)
{
// Reitérer le read()
rc = 0;
}
}
if (rc > 0)
{
assert(l >= (unsigned)rc);
l -= rc;
}
} while (l && (rc >= 0));
if (-1 == rc)
{
int saved_errno = errno;
ROOF_ERR(pCtx, “Error ‘%s’ (%d) on write()\n”,
strerror(errno), errno);
errno = saved_errno;
}
else
{
rc = len;
}
return rc;
} // roof_write
Voici la fonction roof_read() :
static int roof_read(
roof_context_t *pCtx, // Contexte interne
int fd, // Descripteur d’entrée
char *buf, // Buffer de lecture
unsigned int len // Nombre max d’octets à lire
)
{
int rc;
int saved_errno;
do
{
rc = read(fd, buf, len);
if (-1 == rc)
{
if (EINTR == errno)
{
continue;
}
saved_errno = errno;
ROOF_ERR(pCtx, “Error ‘%s’ (%d) on read(%d)\n”,
strerror(errno), errno, fd);
errno = saved_errno
return -1;
}
} while (rc < 0);
return rc;
} // roof_read
Dans les exemples précédents, la variable errno est sauvegardée avant l’appel aux macros de génération des messages d’erreur et restaurée après, car on veut préserver la valeur de l’erreur en sortant de roof_write() et roof_read(). La sauvegarde est obligatoire, car la macro d’erreur fait appel à des fonctions de la bibliothèque C telles que printf() qui peuvent altérer la valeur de errno. C’est une méthode préconisée par le manuel en ligne : man 3 errno.
Ces fonctions auraient pu se contenter d’utiliser l’adresse du buffer d’entrée/sortie stockée dans le contexte interne au lieu de recevoir l’adresse en paramètre. Mais, dans certains cas, elles sont utilisées avec un buffer autre que le buffer d’entrée/sortie du contexte ou alors avec ce buffer, mais à un offset donné lors des lectures des réponses multilignes par exemple.
2.5 Envoi d’une commande
L’une des grandes fonctions du client est d’envoyer des commandes au serveur. Comme il a été vu au § 1.4, les commandes sont composées d’un mot-clé éventuellement suivi d’un paramètre et terminées par les caractères de fin de ligne CR et LF. Les commandes sont envoyées par le client sur le canal de contrôle. C’est la fonction interne roof_send_cmd() qui réalise cette opération :
static int roof_send_cmd(
roof_context_t *pCtx, // Contexte interne
const char *format, // Commande à envoyer
...
)
{
int rc = 0;
va_list args_list;
int sz;
va_start(args_list, format);
sz = vsnprintf(pCtx->iobuf, pCtx->l_iobuf, format, args_list);
va_end(args_list);
[...]
rc = roof_write(pCtx, pCtx->ctrl, pCtx->iobuf, sz);
[...]
return 0;
} // roof_send_cmd
La fonction, qui est à nombre d’arguments variable, reçoit en paramètre une commande spécifiée par un format à la printf() décodé à l’aide du service vsnprintf() de la bibliothèque C. Cela permet de la rendre générique afin d’envoyer toute commande avec ou sans paramètres. Par exemple, pour envoyer une commande sans paramètres comme PASV :
rc = roof_send_cmd(pCtx, "PASV\r\n");
Et pour envoyer une commande avec paramètres comme TYPE :
rc = roof_send_cmd(pCtx, "TYPE %c %c\r\n", type, code);
2.6 Réception d’une réponse
L’autre grande fonction du client est de recevoir des réponses de la part du serveur. Comme indiqué au § 1.5, les réponses ont un format bien déterminé et peuvent être composées d’une ou plusieurs lignes. C’est la fonction roof_get_reply() qui réalise cette opération :
int roof_get_reply(
roof_ctx_t *pContext, // Contexte externe
const char **reply // Réponse
)
{
roof_context_t *pCtx = ROOF_CTX(pContext);
fd_set fdset;
int rc;
struct timeval to;
unsigned int i;
int first = 1;
unsigned int offset = 0;
unsigned int lreply;
char code[4];
*reply = NULL;
lreply = pCtx->l_iobuf;
one_more_time:
FD_ZERO(&fdset);
FD_SET(pCtx->ctrl, &fdset);
if (pCtx->timeout_ms)
{
to.tv_sec = pCtx->timeout_ms / 1000;
to.tv_usec = (pCtx->timeout_ms % 1000) * 1000;
rc = select(pCtx->ctrl + 1, &fdset, NULL, NULL, &to);
}
else
{
rc = select(pCtx->ctrl + 1, &fdset, NULL, NULL, NULL);
}
switch(rc)
{
case -1: // Erreur ou signal
{
if (EINTR == errno)
{
goto one_more_time;
}
ROOF_ERR(pCtx, “Error ‘%s’ (%d) on select()\n”, strerror(errno), errno);
return -1;
}
break;
case 0: // Timeout
{
ROOF_ERR(pCtx, “Timeout on read\n”);
errno = ETIMEDOUT;
return -1;
}
break;
case 1 : // Données en provenance de la connexion
{
char *p;
rc = roof_read_line(pCtx, pCtx->ctrl, pCtx->iobuf + offset, lreply);
[...]
// Si connexion fermée
if (0 == rc)
{
// Ecrasement du buffer avec un code factice
strcpy(pCtx->iobuf, "600");
lreply = pCtx->l_iobuf - 3;
// Ajout d’informations additionnelles si assez de place
strncat(pCtx->iobuf, " End of connection", lreply);
pCtx->iobuf[pCtx->l_iobuf - 1] = ‘\0’;
*reply = pCtx->iobuf;
return 0;
}
[...]
// Recherche de la fin de ligne
p = pCtx->iobuf + offset;
i = 0;
while (p < (pCtx->iobuf + offset + rc))
{
if ((‘\r’ == *p) && (‘\n’ == *(p + 1)))
{
// C’est la 1ère ligne
if (first)
{
// On doit avoir trois digits en début de ligne
[...]
for (i = 0; i < 3; i ++)
{
[...]
code[i] = pCtx->iobuf[i];
} // End for
// Est-ce une réponse multiligne ?
if (‘-’ == pCtx->iobuf[i])
{
first = 0;
// Espace restant dans le buffer d’entrée
lreply -= rc;
// Nouveau début de buffer d’entrée dans le cas
// d’une réponse multiligne
offset += rc;
[...]
// Lecture de la ligne suivante
goto one_more_time;
}
// C’est une réponse monoligne
// Terminer la ligne en écrasant le dernier caractère <CR>
*p = ‘\0’;
*reply = pCtx->iobuf;
return 0;
}
else // C’est une nouvelle ligne d’une réponse multiligne
{
// Conformément à la spécification, une ligne peut commencer
// avec un nombre. Donc, pour vérifier que c’est la dernière
// ligne, on contrôle la valeur du code
if ((rc > 3) &&
(‘ ‘ == ((pCtx->iobuf + offset)[3])) &&
(code[0] == (pCtx->iobuf + offset)[0]) &&
(code[1] == (pCtx->iobuf + offset)[1]) &&
(code[2] == (pCtx->iobuf + offset)[2]))
{
// Fin de réponse multiligne
// Terminer la ligne en écrasant le dernier caractère <CR>
*p = ‘\0’;
*reply = pCtx->iobuf;
return 0;
}
else // Ce n’est pas la fin d’une réponse multiligne
{
// Espace restant dans le buffer d’entrée
lreply -= rc;
// Nouveau début de buffer pour une réponse multiligne
offset += rc;
// Lecture de la ligne suivante
goto one_more_time;
}
}
}
i ++;
p ++;
} // End while
[...]
}
break;
default : // Impossible ???!!!???
{
return -1;
}
} // End switch
return -1;
} // roof_get_reply
La fonction, bien qu’un peu longue, est simple : elle se base sur l’appel système select() pour attendre des données de la part du serveur sur le canal de contrôle (socket ctrl stockée dans le contexte interne). On notera l’utilisation du timeout si celui-ci a été spécifié par l’utilisateur lors de l’appel à roof_new() décrit au § 2.3. Cela évite d’attendre une réponse qui ne viendrait pas si le serveur avait un problème. La fonction est capable de lire des réponses d’une ou plusieurs lignes en testant la présence du tiret derrière le code à 3 digits. Pour lire les lignes de données envoyées par le serveur, la fonction s’appuie sur la routine interne roof_read_line() qui lit des données jusqu’à l’apparition des caractères CR et LF. Il n’est pas très utile de la citer dans l’article.
roof_get_reply() fait partie de l’API, car non seulement elle est utilisée en interne comme nous le verrons, mais peut aussi être utilisée en externe par l’utilisateur désireux de réceptionner les messages spontanés de la part du serveur. C’est donc pour cela qu’elle reçoit en paramètre un contexte externe et non pas interne.
Nous verrons dans les fonctions suivantes que le traitement des réponses de la part du serveur consiste à ne regarder que le premier digit du code à 3 chiffres, car il suffit à lui seul pour déterminer les actions à entreprendre par la suite (cela est précisé au § 4.2 de la RFC959).
2.7 Connexion au serveur
La connexion au serveur consiste tout d’abord à établir le canal de contrôle conformément à ce qui est indiqué au début du § 5.4 de la RFC959 : le canal de contrôle TCP est ouvert et le serveur envoie une réponse avec le code 220 pour indiquer qu’il est prêt. S’il n’est pas prêt à recevoir des commandes, le serveur envoie une réponse 120 et le client est censé attendre jusqu’à réception de la réponse 220.
C’est la mission de la fonction de service roof_open_ctrl() qui reçoit en paramètres l’adresse du serveur (qui peut être un nom de machine ou une adresse IP) et son numéro de port TCP. Si le serveur est en attente sur le port standard (ce qui est généralement le cas), l’utilisateur a la possibilité d’utiliser la constante ROOF_DEF_PORT au lieu de la valeur en dur 21. Le code de retour de ce service est la socket sur le canal de contrôle ou -1 en cas d’erreur. Le champ ctrl du contexte interne mémorise l’identifiant de socket. On remarquera l’utilisation de l’option de socket SO_LINGER pour faire en sorte que le protocole TCP donne toutes les chances à l’autre partie pour récupérer les données en attente avant fermeture définitive de la connexion.
int roof_open_ctrl(
roof_ctx_t *pContext, // Contexte externe
const char *host, // Adresse du serveur (nom ou adresse IP)
unsigned int port // Numéro de port du serveur
)
{
roof_context_t *pCtx = ROOF_CTX(pContext);
int rc;
int sd = -1;
struct sockaddr_in addr;
struct hostent *pHost;
char *code;
struct linger opt_linger;
int err_sav;
[...]
// Obtention d‘un descripteur de socket TCP
sd = socket(PF_INET, SOCK_STREAM, 0);
[...]
// Positionnement de l’option SO_LINGER pour faire en sorte que l’appelant
// de close() sur la socket attende jusqu’à ce que l’autre partie ait
// récupéré toutes les données en attente sur la connexion
opt_linger.l_onoff = 1; // Activate the LINGER
opt_linger.l_linger = 2; // Persistence tme in 100ms units
rc = setsockopt(sd, SOL_SOCKET, SO_LINGER,
&opt_linger, sizeof(opt_linger));
[...]
// Conversion du nom de host en adresse
pHost = gethostbyname(host);
[...]
// Renseignement de l’adresse
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = (in_addr_t)(*(unsigned long *)(pHost->h_addr_list[0]));
//Connexion au host distantrc = connect(sd, (struct sockaddr *)&addr, sizeof(addr)); [...] // On renseigne le contexte maintenant car roof_get_reply() a besoin que // le champ ctrl soit renseignépCtx->ctrl = sd;//On boucle jusqu’à obtention de la réponse 2yz ou timeoutdo { // Attente d’une réponse de la part du serveur rc = roof_get_reply(pContext, &code); [...] if ((code[0] != ‘1’) && (code[0] != ‘2’)) { ROOF_ERR(pCtx, "Negative reply code ‘%s’\n", pCtx->iobuf); errno = EIO; rc = -1; goto error; } } while (code[0] != ‘2’); rc = sd; goto end; error: err_sav = errno; if (sd >= 0) { close(sd); sd = -1; } pCtx->ctrl = -1; errno = err_sav; end: return rc; } // roof_open_ctrl
La deuxième et dernière phase de la connexion au serveur est la mise en œuvre de la procédure d’identification. La RFC959 propose un diagramme qui est reproduit (avec quelques adaptations) en figure 5. Le diagramme montre que, en fonction des serveurs, l’identification se contentera de la commande USER ou alors de la commande USER suivie de la commande PASS ou enfin de la commande USER suivie de la commande PASS suivie de la commande ACCT. Dans la pratique, le cas le plus courant est USER suivi de PASS.

Figure 5 : Diagramme d’identification
C’est la fonction de service roof_login() qui met en œuvre l’automate d’identification. Nous citons cette fonction afin de montrer que les commandes sont envoyées par la routine interne roof_send_cmd() vue plus haut, les réponses sont réceptionnées par le service roof_get_reply() vu plus haut et les diagrammes de la recommandation sont implémentés sous forme d’une série de switch/case pour tester les valeurs des réponses et de goto pour changer d’état.
int roof_login(
roof_ctx_t *pContext, // Contexte externe
const char *login, // Nom de login
const char *passwd, // Mot de passe
const char *account // Informations de compte
)
{
roof_context_t *pCtx = ROOF_CTX(pContext);
int rc;
const char *code;
assert(NULL != pCtx);
assert(pCtx->busy);
if (!login || !(login[0]))
{
ROOF_ERR(pCtx, "NULL login parameter\n");
errno = EINVAL;
return -1;
}
rc = roof_send_cmd(pCtx, “USER %s\r\n”, login);
[...]
rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code);
[...]
switch(code[0]) {case ‘1’: // Réponse positive préliminairecase ‘4’: // Terminaison négative transitoirecase ‘5’: // Terminaison négative permanente { ROOF_ERR(pCtx, "Error ‘%s’\n", pCtx->iobuf); errno = EIO; return -1; } break; case ‘3’ : // Réponse positive intermédiaire {goto send_passwd;} break;case ‘2’: // Terminaison positive {goto end; } break; default : // Normalement impossible { ROOF_ERR(pCtx, “Unexpected reply code ‘%s’\n”, pCtx->iobuf); errno = EIO; return -1; } break; } // End switchsend_passwd:if (!passwd || !(passwd[0])) { ROOF_ERR(pCtx, “Password parameter is required by server\n”); errno = EINVAL; return -1; } rc =roof_send_cmd(pCtx, “PASS %s\r\n”, passwd); [...] rc =roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...] switch(code[0]) { case ‘1’ : // Positive Preliminary reply case ‘4’ : // Terminaison négative transitoire case ‘5’ : // Terminaison négative permanente { ROOF_ERR(pCtx, "Error ‘%s’\n", pCtx->iobuf); errno = EIO; return -1; } break;case ‘3’: // Réponse positive intermédiaire {goto send_account;} break;case ‘2’: // Terminaison positive {goto end;} break; default : // Normalement impossible { ROOF_ERR(pCtx, “Unexpected reply code ‘%s’\n”, pCtx->iobuf); errno = EIO; return -1; } break; } // End switchsend_account:if (!account || !(account[0])) { ROOF_ERR(pCtx, “Account parameter is required by server\n”); errno = EINVAL; return -1; } rc = roof_send_cmd(pCtx, “ACCT %s\r\n”, account); [...] rc =roof_get_reply(ROOF_EXT_CTX(pCtx), &code); [...]switch(code[0]) {case ‘1’: // Positive Preliminary replycase ‘3’ : // Réponse positive intermédiaire case ‘4’ : // Terminaison négative transitoirecase ‘5’: // Terminaison négative permanente { ROOF_ERR(pCtx, "Error ‘%s’\n", pCtx->iobuf); errno = EIO; return -1; } break; case ‘2’ : // Terminaison positive { goto end; } break; default : // Normalement impossible { ROOF_ERR(pCtx, “Unexpected reply code ‘%s’\n”, pCtx->iobuf); errno = EIO; return -1; } break; } // End switch end: return 0; } // roof_login
2.8 Diagramme numéro 1
Le diagramme de la figure 6 est le premier présenté au § 6 de la RFC959. Il concerne les commandes ABOR, ALLO, DELE, CWD, CDUP, SMNT, HELP, MODE, NOOP, PASV, QUIT, SITE, PORT, SYST, STAT, RMD, MKD, PWD, STRU et TYPE.

Figure 6 : Diagramme numéro 1
2.9 Diagramme numéro 2
Le diagramme de la figure 7 est le second présenté au § 6 de la RFC959. Il concerne les commandes de transfert des données APPE, LIST, NLST, REIN, RETR, STOR et STOU.

Figure 7 : Diagramme numéro 2
Cet automate introduit la fonction roof_open_data() qui ouvre le canal de données afin de transférer le contenu des fichiers ou répertoires :
static int roof_open_data(roof_context_t *pCtx) // Contexte interne
{
int rc;
const char *code;
char *p;
int port_lsb, port_msb, port;
struct sockaddr_in addr;
int data;
int err_sav;
rc = roof_send_cmd(pCtx, “PASV\r\n”);
[...]
rc = roof_get_reply(ROOF_EXT_CTX(pCtx), &code);
[...]
// S’assurer que la réponse est OK
if (code[0] != ‘2’)
{
ROOF_ERR(pCtx, “Error ‘%s’\n”, pCtx->iobuf);
errno = EIO;
return -1;
}
// Parsing de la réponse du serveur pour obtenir le numéro de port
// et l’adresse du serveur
p = pCtx->iobuf;
while (*p && (*p != ‘)’))
{
p ++;
}
if (*p != ‘)’)
{
ROOF_ERR(pCtx, “Expected a terminating ‘)’ in ‘%s’\n”, pCtx->iobuf);
errno = EIO;
return -1;
}
*p = ‘\0’;
while ((p != pCtx->iobuf) && (*p != ‘,’))
{
p --;
}
if ((*p != ‘,’) && (!isdigit(*(p+1))))
{
ROOF_ERR(pCtx, „Expected a ‚,‘ followed by a digit in ‚%s‘\n“, pCtx->iobuf);
errno = EIO;
return -1;
}
*p = ‘\0’;
port_lsb = atoi(p+1);
while ((p != pCtx->iobuf) && (*p != ‘,’))
{
p --;
}
if ((*p != ‘,’) && (!isdigit(*(p+1))))
{
ROOF_ERR(pCtx, „Expected a ‚,‘ followed by a digit in ‚%s‘\n“, pCtx->iobuf);
errno = EIO;
return -1;
}
*p = ‘\0’;
port_msb = atoi(p+1);
port = (port_msb << 8) | port_lsb;
// Conversion de l’adresse en format " point "
p--;
while ((p != pCtx->iobuf) && (*p != ‘(‘))
{
if (‘,’ == *p)
{
*p = ‘.’;
}
else
{
if (!isdigit(*p))
{
ROOF_ERR(pCtx, "Expected a digit in the server’s address’%s’\n",
pCtx->iobuf);
errno = EIO;
return -1;
}
}
p --;
}
if (*p != ‚(‚)
{
ROOF_ERR(pCtx, „Expected a ‚(‚ in ‚%s‘\n“, pCtx->iobuf);
errno = EIO;
return -1;
}
// Renseignement de l’adresse du serveur
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(p+1);
// Création de la socket pour le canal de données
data = socket(PF_INET, SOCK_STREAM, 0);
[...]
// Connexion au serveur
rc = connect(data, (struct sockaddr *)&addr, sizeof(addr));
[...]
return data;
} // roof_open_data
La commande PASV est envoyée au serveur pour le faire passer en mode passif. Le serveur répond par un code " 2 " du style 227 Entering Passive Mode (127,0,0,1,128,87) pour donner le numéro de port sur lequel il va attendre une connexion du client. Les nombres entre parenthèses séparés par des virgules sont normalisés dans la RFC959. Ils représentent de gauche à droite l’adresse IP (les quatre premiers champs, soit 127.0.0.1), le poids fort et le poids faible du numéro de port codé sur 16 bits (soit 128 * 256 + 87 = 32855). Une fois ces informations obtenues, une socket est créée pour établir le canal de données avec le serveur. L’identifiant de la socket est le code de retour de la fonction si tout s’est bien passé (sinon l’erreur -1 est retournée).
Nous ne décrirons pas ici les fonctions qui implémentent les commandes se basant sur le diagramme numéro 2, car elles utilisent le même principe que roof_login() vue plus haut.
2.10 Diagramme numéro 3
Le diagramme de la figure 8 est le troisième présenté au § 6 de la RFC959. Il concerne les commandes de renommage de fichier RNFR et RNTO. Cet automate est implémenté dans la fonction de service roof_mv().

Figure 8 : Diagramme numéro 3
3. Exemple d’utilisation de l’API
Pour un exemple complet, le lecteur pourra se référer au code source de ROOF : le sous-répertoire client contient un exemple de client FTP en mode ligne de commande se basant sur l’API.
Ici est présenté un tout petit programme d’exemple appelé test_ftp qui effectue une connexion vers un serveur, affiche le type du système hôte, affiche le nom du répertoire de travail et liste le contenu de ce dernier :
#include <stdio.h>
#include <libgen.h>
#include <assert.h>
#include <unistd.h>
// Installe par defaut dans ‘/usr/local/include’
#include <roof.h>
// Callback pour afficher le contenu d’un repertoire
//
// Parametres: ctx = parametre ‘ctx’ passe a roof_list()
// buf = Donnees du directory
// lbuf = taille des donnees dans ‘buf’
static int affiche(roof_ctx_t *ctx, char *buf, unsigned int lbuf)
{
int rc;
(void)ctx; // Parametre non utilise (suppression warning compilo)
rc = write(1, buf, lbuf);
assert(lbuf == (unsigned)rc);
// Retour OK a la librairie
return lbuf;
} // affiche
// Point d’entree du programme
//
// Parametres: av[1] = hote_destination
// av[2] = nom_de_login
// av[3] = mot_de_passe
int main(int ac, char *av[])
{
roof_ctx_t *pCtx; // Objet ROOF
int ctrl; // Socket sur le canal de contrôle
char *p;
int rc;
// Validation des parametres du programme
if (ac != 4)
{
fprintf(stderr, "Usage: %s host login passwd\n", basename(av[0]));
return 1;
}
// Creation de l’objet ROOF avec: // . Timeout 10 secondes // . Allocation du buffer d’E/S par la librairie // . Pas de contexe utilisateur pCtx = roof_new(10000, NULL, 0, NULL); assert(pCtx); // Ouverture du canal de controle ctrl = roof_open_ctrl(pCtx, av[1], ROOF_DEF_PORT); assert(ctrl >= 0); // Authentification sur le hote distant rc = roof_login(pCtx, av[2], av[3], NULL); assert(0 == rc); // Affichage du type du systeme hote rc = roof_syst(pCtx, &p); assert(0 == rc); printf(“Le type du systeme hote est : %s\n”, p); // Affichage du chemin du repertoire courant rc = roof_pwd(pCtx, &p); assert(0 == rc); printf(“Le repertoire courant est : %s\n”, p); // Affichage du contenu du repertoire courant printf(“Le contenu du repertoire courant est :\n”); rc = roof_list(pCtx, NULL, affiche); assert(0 == rc); // Fermeture du canal de controle rc = roof_close_ctrl(pCtx); assert(0 == rc); // Desallocation de l’objet ROOF roof_delete(pCtx); return 0; } // main
Le programme peut être généré comme suit si la bibliothèque libroof.so a été installée :
$ gcc test_ftp.c -o test_ftp -lroof
Voici, pour finir, un exemple d’exécution du programme de test avec connexion au serveur de la machine locale (localhost), avec le nom de login foo et le mot de passe bar :
$ ./test_ftp localhost foo bar Le type du systeme hote est : 215 UNIX Type: L8 (Linux) Le repertoire courant est : 257 “/home/foo” is current directory. Le contenu du repertoire courant est : total 2256 -rw-r--r-- 1 foo foo 60 Jan 16 08:55 fichier1 -rw------- 1 foo foo 203 Jan 16 08:55 fichier2 drwx------ 2 foo foo 4096 Mar 11 2007 repertoire $
Conclusion
Après une étude rapide de la recommandation FTP, il a été possible de développer une API appelée ROOF [3] très simple à mettre en œuvre et adaptable à toute application nécessitant le transfert de fichiers par un protocole standard. Comme exemples d’applications dans lesquelles le lecteur pourrait se lancer, on peut citer un client FTP en mode graphique ou un système de fichiers distant basé sur FUSE [7] comme alternative à NFS.
Pour approfondir le sujet, le lecteur pourra aussi s’intéresser aux évolutions du protocole, car FTP a fait l’objet de diverses améliorations depuis sa création et, notamment, sur le plan de la fiabilité et de la sécurité pour donner : SFTP (" FTP over SSH " à ne pas confondre avec " Simple FTP ") ou FTPS (" FTP over SSL ").
Liens
- [1] Recommandation FTP : http://www.ietf.org/rfc/rfc959.txt
- [2] Recommandation TELNET : http://www.ietf.org/rfc/rfc854.txt
- [3] Remote Operations On Files : http://roof.sourceforge.net/
- [4] Aperçu de cmake : http://rachid.koucha.free.fr/tech_corner/cmake_manual.html
- [5] Constructeurs/destructeurs dans les bibliothèques partagées : http://tldp.org/HOWTO/Program-Library-HOWTO/miscellaneous.html#INIT-AND-CLEANUP
Références
- [6] COURBOT (Alexandre), " Cmake : la relève dans la construction de projets ", GLMF 92, mars 2007.
- [7] TRICON (Lionel), " FUSE, développez vos systèmes de fichiers dans l’espace utilisateur ", GLMF 92, mars 2007.
Retrouvez cet article dans : Linux Magazine 103


