• 6 hours
  • Hard

Free online content available in this course.

course.header.alt.is_certifying

Got it!

Last updated on 2/6/20

TP : explorez un serveur FTP

Log in or subscribe for free to enjoy all this course has to offer!

Afin de mettre en pratique tout ce que vous avez appris depuis le début (oui, vous savez déjà beaucoup de choses), je vous propose de coder un petit client FTP qui va vous permettre d'explorer un serveur FTP.

Je ne vais pas vous demander de refaire FileZilla, je vous rassure tout de suite, mais je pense que cet exercice sera très enrichissant et vous permettra de pratiquer un peu avant d'attaquer la suite. :)

Avant de commencer

Déjà, avant de commencer, vous devez avoir accès à un FTP. Le plus simple est encore d'en installer un sur votre machine.
Pour cela, téléchargez FileZilla Server et installez-le.
Une fois FileZilla lancé, vous devriez avoir ceci sous les yeux :

Image utilisateur
Image utilisateur

Si vous avez laissé les choix par défaut, vous pouvez vous connecter au serveur 127.0.0.1 sans mot de passe.

Une fois fait, nous allons devoir créer un compte sur lequel nous allons attribuer un répertoire pour que ce compte puisse ajouter des fichiers, les supprimer etc.
Pour ajouter un compte, cliquez d'abord sur le bouton d'administration des comptes utilisateurs :

Image utilisateur

Nous allons ajouter un compte :

Image utilisateur
Image utilisateur

Nous devons maintenant lui affecter un dossier où déposer des fichiers. Notez que vous devez avoir créé votre dossier à partager au préalable. Celui que j'ai créé est dénommé "C:\FTP" :

Image utilisateur

Ceci fait, nous pouvons tester cette configuration avec FileZilla (client). Voici ce que j'obtiens :

Image utilisateur

L’application que vous allez réaliser dans ce TP permettra de se déplacer dans un répertoire et d’en lister le contenu. On ne s’occupera pas de pouvoir créer un répertoire, ni d’uploader ou de télécharger des documents.
Pour pouvoir tester l'application, vous allez devoir créer des dossiers et des fichiers sur notre FTP. Voici ce que j'ai mis dans le mien : 

  • un dossier "upload" contenant quatre fichiers ;

  • un dossier "download" contenant également quatre fichiers.

Je vous propose de créer 2 dossiers du même nom, et d'y inclure les fichiers que vous voulez.

Bref rappel sur le protocole FTP

Maintenant, vous allez devoir vous familiariser avec les commandes FTP, celles que vous allez utiliser pour communiquer avec votre serveur.
Voici un lien vers la RFC qui liste tout ce qu'il est possible de faire.

Le protocole FTP offre un certain nombre de commandes afin de pouvoir communiquer avec lui. Voici la liste des commandes que vous allez utiliser :

  • USER <login> : identifie un utilisateur ;

  • PASS <passwd> : spécifie son mot de passe ;

  • QUIT : déconnecte de la session en cours ;

  • PWD : affiche le chemin courant sur le FTP ;

  • CWD <répertoire> : permet d'aller dans le répertoire choisi ;

  • PASV : entre en mode passif ;

  • TYPE ASCII : passe en mode de communication ASCII - OBLIGATOIRE pour que la commande LIST fonctionne ;

  • LIST : retourne le contenu du répertoire courant.

Voici comment les choses devront se passer :

  1. Créez la socket ;

  2. Récupérez la réponse du serveur FTP : la réponse doit commencer par 220 ;

  3. Envoyez la commande USER <login> ;

  4. Récupérez la réponse du serveur FTP : la réponse doit commencer par 331 ;

  5. Envoyez la commande PASS <passwd> ;

  6. Récupérez la réponse du serveur FTP : la réponse doit commencer par 230.

Vous êtes maintenant connectés !

Avec cette socket vous pourrez utiliser les commandes PWD, CWD, QUIT, PASV et TYPE, mais pas LIST !

Et pourquoi pas LIST ?

Parce que le protocole FTP, pour certaines requêtes comme l'upload de documents ou le listing d'un répertoire, demande à ce que nous utilisons une autre socket (on parle de canal de données). Celle-ci peut être définie par nos soins (mode actif avec la commande PORT) ou définie par le serveur (mode passif). Nous utiliserons ici le mode passif car l'utilisation de la commande port nécessite l'utilisation d'un objet ServerSocket, et nous ne l'avons pas encore vu. ;) Il faudra aussi s'assurer que le type de communication sur le canal de données est du type ASCII, il faudra donc utiliser la commande TYPE ASCII avant d'entrer en mode passif.
Ce canal de données sert à faire transiter des données et le canal de base sert à faire transiter les commandes passées au serveur. Voici comment initialiser ce canal :

Initialisation de la seconde socket
Initialisation de la seconde socket

Qu'est-ce que c'est que cette réponse du serveur à la commande PASV ?

Cette réponse nous fournit l'adresse IP du serveur et le port d'écoute pour le canal de données mais celui-ci a été découpé en deux.
Pour pouvoir récupérer la vraie valeur numérique pour ce port, nous allons devoir utiliser une petite gymnastique informatique. Je vous offre donc le morceau de code qui permet de parser la réponse à la commande PASV :

int debut = response.indexOf('(');
     int fin = response.indexOf(')', debut + 1);
     if(debut > 0){
        String dataLink = response.substring(debut + 1, fin);
        StringTokenizer tokenizer = new StringTokenizer(dataLink, ",");
        try {
           //L'adresse IP est séparée par des virgules
           //on les remplace donc par des points...
           ip = tokenizer.nextToken() + "." + tokenizer.nextToken() + "."
                   + tokenizer.nextToken() + "." + tokenizer.nextToken();
          
           //Le port est un entier de type int
           //mais cet entier est découpé en deux
           //Il faut multiplier le premier nombre par 256 et l'additionner au second
           //pour avoir le numéro du port ouvert par le serveur
           port = Integer.parseInt(tokenizer.nextToken()) * 256
                + Integer.parseInt(tokenizer.nextToken());
           dataIP = ip;
           dataPort = port; 
          
        } catch (Exception e) {
          throw new IOException("SimpleFTP received bad data link information: "
              + response);
        }        
     }

Votre mission

Je ne vais pas vous donner toutes les réponses quand même. Il faudra que vous cherchiez un peu par vous-même comment mettre en place ce genre de service.
Étant d'un naturel tolérant, je ne vous demande qu'une application en mode console où vous pourrez vous connecter à votre FTP, lister votre contenu et vous balader dans les répertoires.

Voici ce que j'ai obtenu :

Mon résultat
Mon résultat

Prêt ? Alors à vos claviers !

Correction

Voici une solution possible pour ce TP :

package test2;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.StringTokenizer;

public class Ftp {

   private String user;
   private Socket socket = null, socketData = null;
   private boolean DEBUG = true;
   private String host;
   private int port;
   
   private BufferedWriter writer, writerData;
   private BufferedInputStream readerData;
   private BufferedInputStream reader;
   private String dataIP;
   private int dataPort;

   
   public Ftp(String ipAddress, int pPort, String pUser){
      user = pUser;
      port = pPort;
      host = ipAddress;
   }
   public Ftp(String pUser){
      this("127.0.0.1", 21, pUser);      
   }
   
   /**
    * Méthode de connexion au FTP
    * @throws IOException
    */
   public void connect() throws IOException{
      //Si la socket est déjà initialisée
      if(socket != null)
           throw new IOException("La connexion au FTP est déjà activée");
      
      //On se connecte
      socket = new Socket(host, port);
      
      //On crée nos objets pour pouvoir communiquer
      reader = new BufferedInputStream(socket.getInputStream());
      writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
      
      String response = read();      

      if(!response.startsWith("220")){  
         throw new IOException("Erreur de connexion au FTP : \n" + response);
      }
      send("USER " + user);
      response = read();
      if(!response.startsWith("331"))  
         throw new IOException("Erreur de connexion avec le compte utilisateur : \n" + response);
      
      //Pas de gestion du mot de passe dans notre cas, mais vous pouvez en préciser un dans le string passwd si vous le souhaitez
      String passwd = "";
      send("PASS " + passwd);
      response = read();
      if(!response.startsWith("230"))  
         throw new IOException("Erreur de connexion avec le compte utilisateur : \n" + response);

      //Nous sommes maintenant connectés
   }
   
   public void quit(){
      try {
         send("QUIT");
      } catch (IOException e) {
         e.printStackTrace();
      }  finally{
         if(socket != null){
            try {
               socket.close();
            } catch (IOException e) {
               e.printStackTrace();
            }
            finally{
               socket = null;
            }
         }
      }
   }
   
   /**
    * Méthode permettant d'initialiser le mode passif
    * et ainsi de pouvoir communiquer avec le canal dédié aux données
    * @throws IOException
    */
   private void enterPassiveMode() throws IOException{

     send("PASV");
     String response = read();
     if(DEBUG)
        log(response);
     String ip = null;
     int port = -1;
     
     //On décortique ici la réponse retournée par le serveur pour récupérer
     //l'adresse IP et le port à utiliser pour le canal data
     int debut = response.indexOf('(');
     int fin = response.indexOf(')', debut + 1);
     if(debut > 0){
        String dataLink = response.substring(debut + 1, fin);
        StringTokenizer tokenizer = new StringTokenizer(dataLink, ",");
        try {
           //L'adresse IP est séparée par des virgules
           //on les remplace donc par des points...
           ip = tokenizer.nextToken() + "." + tokenizer.nextToken() + "."
                   + tokenizer.nextToken() + "." + tokenizer.nextToken();
          
           //Le port est un entier de type int
           //mais cet entier est découpé en deux
           //la première partie correspond aux 4 premiers bits de l'octet
           //la deuxième au 4 derniers
           //Il faut donc multiplier le premier nombre par 256 et l'additionner au second
           //pour avoir le numéro de ports défini par le serveur
           port = Integer.parseInt(tokenizer.nextToken()) * 256
                + Integer.parseInt(tokenizer.nextToken());
           dataIP = ip;
           dataPort = port; 
          
        } catch (Exception e) {
          throw new IOException("SimpleFTP received bad data link information: "
              + response);
        }        
     }
   }
   
   /**
    * Méthode initialisant les flux de communications
    * @throws UnknownHostException
    * @throws IOException
    */
   private void createDataSocket() throws UnknownHostException, IOException{
      socketData = new Socket(dataIP, dataPort);
      readerData = new BufferedInputStream(socketData.getInputStream());
      writerData = new BufferedWriter(new OutputStreamWriter(socketData.getOutputStream()));
   }
   
   /**
    * Retourne l'endroit où nous nous trouvons dur le FTP
    * @return
    * @throws IOException
    */
   public String pwd() throws IOException{
      //On envoie la commande
      send("PWD");
      //On lit la réponse
      return read();
   }
   
   /**
    * Permet de changer de répertoire (Change Working Directory)
    * @return
    * @throws IOException
    */
   public String cwd(String dir) throws IOException{
      //On envoie la commande
      send("CWD " + dir);
      //On lit la réponse
      return read();
   }
   
   public String list() throws IOException{
      send("TYPE ASCII");      
      read();
      
      enterPassiveMode();
      createDataSocket();
      send("LIST");
      
      return readData();
   }
   
   /**
    * Méthode permettant d'envoyer les commandes au FTP
    * @param command
    * @throws IOException
    */
   private void send(String command) throws IOException{
      command += "\r\n";
      if(DEBUG)
         log(command);
      writer.write(command);
      writer.flush();
   }
   
   /**
    * Méthode permettant de lire les réponses du FTP
    * @return
    * @throws IOException
    */
   private String read() throws IOException{      
      String response = "";
      int stream;
      byte[] b = new byte[4096];
      stream = reader.read(b);
      response = new String(b, 0, stream);
      
      if(DEBUG)
         log(response);
      return response;
   }
      
   /**
    * Méthode permettant de lire les réponses du FTP
    * @return
    * @throws IOException
    */
   private String readData() throws IOException{
      
      String response = "";
      byte[] b = new byte[1024];
      int stream;
      
      while((stream = readerData.read(b)) != -1){
         response += new String(b, 0, stream);
      }
      
      if(DEBUG)
         log(response);
      return response;
   }
   
   
   public void debugMode(boolean active){
      DEBUG = active;
   }
   
   private void log(String str){
      System.out.println(">> " + str);
   }
   
}
package test2;

import java.io.IOException;
import java.util.Scanner;

import test2.Ftp;

public class Main {

   public static void main(String[] args) {

      try {
         Scanner sc = new Scanner(System.in);
         Ftp ftp = new Ftp("cysboy");
         System.out.println("Connexion au FTP");

         ftp.connect();
         ftp.debugMode(true);
         
         System.out.println("-------------------------------------------------------");
         System.out.println("Vous êtes maintenant connecté au FTP");
         System.out.println("Vous avez le droit aux commandes PWD, CWD, LIST et QUIT");
         System.out.println("-------------------------------------------------------\n\n");
         String reponse = "";
         boolean encore = true;
         while (encore) {
            reponse = sc.nextLine().toUpperCase();

            switch (reponse) {
            case "PWD":
               System.out.println(ftp.pwd());
               break;
            case "CWD":
               System.out.println(">> Saisissez le nom du répertoire où vous voulez aller : ");
               String dir = sc.nextLine();
               System.out.println(ftp.cwd(dir));
               break;
            case "LIST":
               String list = ftp.list();
               System.out.println(list);
               break;
            case "QUIT":
               ftp.quit();
               encore = false;
               break;
             default : 
                System.err.println("Commande inconnue !");
                break;
            }

         }
      } catch (IOException e) {
         e.printStackTrace();
         System.exit(0);
      }
      
      System.out.println("Ciao ! ");
   }
}

Vous êtes toujours vivant ? Vous avez survécu à cette dure épreuve ? J'espère que ce TP vous a plu. :)
Alors continuons !

Example of certificate of achievement
Example of certificate of achievement