State.java

package fr.umlv.ji.tcp.server.ftp;
import java.util.logging.*;
import java.net.*;
import java.io.*;
import fr.umlv.ji.tcp.server.*;
import java.lang.reflect.*;
/**
 * Classe définissant l'état d'une connexion TCP et encapsulant
 * le traitement des requêtes FTP sur cette connexion.
 */
public class State {
  /** Nom du système. */
  protected static String osName;
  /** Serveur. */
  protected Server server;
  /** Flot en écriture. */
  protected PrintStream output;
  /** Flot en lecture. */
  protected LineNumberReader input;
  /** Adresse de la socket du cliente pour le transfert de données. */
  protected InetSocketAddress clientDataAddress;
  /** Adresse de la socket du serveur pour le transfert de données. */
  protected InetSocketAddress serverDataAddress;
  /** Tampon pour le transfert de données. */
  byte[] buffer = new byte[2048];
  /** Socket pour le transfert des données. */
  protected Socket dataSocket = null; 
  /** Nom du client. */
  protected String user = null;
  /** Vrai si le client est authentifié. */
  protected boolean login = false;
  /** Type de transfert. */
  protected String type = "A";
  /** Indice de départ pour le transfert. */
  protected int restartIndex = 0;
  /** Répertoire racine. */ 
  protected File root;
  /** Répertoire de travail. */
  protected File wdir;
  /** Connexion terminée. */
  protected boolean closed = false;
  /** Constructeur du gestionnaire de requête FTP.
      @param server serveur associé au gestionnaire.
      @param socket socket pour la réception des requêtes. */
  public State(Server server, Socket socket) throws IOException {
    this.server = server;
    // Initialisation des flots associés à la socket
    output = new PrintStream(socket.getOutputStream(),true);
    input = new LineNumberReader(
             new InputStreamReader(socket.getInputStream(),"ASCII"));
    // Initialisation des adresses de socket pour le transfert de données.
    clientDataAddress =
      (InetSocketAddress)socket.getRemoteSocketAddress();
    serverDataAddress =
      new InetSocketAddress(socket.getLocalAddress(),socket.getPort()-1);
    // Initialisation du répertoire racine
    root = new File(server.getPreferences().get("rootDir","."));
    server.getLogger().info("Root directory is: "+ root);
    root = root.getCanonicalFile();
    wdir = root;
    // Initialisation du système
    osName = System.getProperty("os.name").toUpperCase();
    if (osName==null)
      osName = "INCONNU";
    else 
      if (osName.equals("LINUX")) 
    osName = "UNIX";
    // Envoi d'un message d'accueil
    output.println("220 Bienvenue\r");
  }
  /** Indique si la connexion est fermée. */
  public boolean isClosed() { return closed; }
  /** Méthode de traitement d'une requête. */ 
  public void treatRequest() throws IOException {
    String line = input.readLine();
    if (line==null)
      throw new IOException();
    // Récupération du premier mot de la requête
    String request;
    String reste;
    int index = line.indexOf(' ');
    if (index!=-1) {
      request = line.substring(0,index);
      reste = line.substring(index+1);
    } else {
      request = line;
      reste = "";
    }
    request = request.toUpperCase();
    // Appel dynamique de la méthode dont le nom est le premier mot
    // de la requête suivi de Handler (par exemple, PASSHandler).
    // Le reste de la requête est passé en paramètre de la méthode
    // appelée
    try {
      server.getLogger().info(line);
      String methodName = request + "Handler";
      Method method = State.class.getMethod(methodName,argType);
      args[0] = reste;
      method.invoke(this, args);
    } catch (Exception e) {
      output.println("500 '"+ line +"': Requête incomprise.\r");
      server.getLogger().log(Level.WARNING,line,e);
    }
  }
  // Cette méthode requiert les attributs suivants:
  /** Tableau des types des arguments de la méthode à invoquer. */
  static Class[] argType = new Class[]{String.class};
  /** Tableau des arguments de la méthode à invoquer. */
  Object[] args = new Object[1];
  /** Traitement de la requête USER.
      @param param argument de la requête. */
  public void USERHandler(String param) {
    if (!login) {
      user = param;
      output.println("331 Vous devez donner votre mot de passe, "
             + user + ".\r");
    } else {
      output.println("530 Déjà logé.\r");
    }
  }
  /** Traitement de la requête PASS.
      @param param argument de la requête. */
  public void PASSHandler (String param) throws IOException {
    if (user==null || login) {
      output.println("503 Utilisez d'abord USER.\r");
      return;
    }
    // Vérification du mot de passe
    login = checkPasswd(user,param);
    if (login) {
      output.println("230 Utilisateur "+ user + " logé.\r");
      return;
    }
    user = null;
    output.println("530 Login incorrect.\r");
  }
  /** Traitement de requête PWD. Retourne au client le répertoire courant.
      @param param argument de la requête. */
  public void PWDHandler (String param) {
     if (!login) {
       output.println("530 Logez vous avec USER et PASS.\r");
       return;
     }
     output.println("257 "+ wdir +" est le répertoire courant.\r");
  }
  /** Retourne au client le nom du système.
      @param param argument de la requête. */
  public void SYSTHandler(String param) {
    output.println("215 " + osName+"\r");
  }
  /** Précise le type de tranfert. Tous les types sont acceptés,
      mais seul le transfert binaire « I » est implanté.
      @param param argument de la requête.  */
  public void TYPEHandler(String param) throws IOException  {
    type = param;
    output.println("200 Le type est " + type + ".\r");
  }
  /** Change le répertoire courant.
      @param param nouveau répertoire. */
  public void CWDHandler(String param) throws IOException {
    // Vérifier que le client est logé
    if (!login) {
      output.println("530 Logez vous avec USER et PASS.\r");
      return;
    }
    // Rechercher le répertoire demandé
    if (param.equals("~")) {
      wdir = root;
    } else {
      File tmp = new File(wdir, param).getCanonicalFile();
      if (!(tmp.exists() && tmp.isDirectory())) {
    output.println("550 "+param+": Aucun répertoire de ce nom.\r");
    return;
      }
      wdir = tmp;
    }
    output.println("200 Le répertoire courant est " + wdir + ".\r");
    return;
  }
  /** Passe le serveur en mode attente de connexion.
      @param param argument de la requête. */
  public void PASVHandler(String param) throws IOException {
    // Vérifier que le client est logé
    if (!login) {
      output.println("530 Logez vous avec USER et PASS.\r");
      return;
    }
    // Création d'une socket serveur pour l'émission des données.
    // Le serveur doit attacher sa socket sur son adresse Internet
    // et sur un port libre. Il doit ensuite envoyer un message au
    // client (dans un format défini dans la norme) pour l'informer
    // de l'adresse et du port sur lesquels le client devra se connecter
    // pour recevoir les données.
    ServerSocket dataServer = null;
    try {
      dataServer = new ServerSocket(0);
      String a;
      // Récupération de l'adresse Internet du serveur
      try {
    a = InetAddress.getLocalHost().getHostAddress().replace('.',',');
      } catch (Exception e) {
    // Pour que l'exemple fonctionne en local
    a = "127,0,0,1";
      }
      String port = "," + dataServer.getLocalPort()/256;
      port += "," + dataServer.getLocalPort()%256;
      output.println("227 Entrée en mode passif (" + a + port + ")\r");
      dataServer.setSoTimeout(10000);
      dataSocket = dataServer.accept();
    } catch (IOException e) {
      output.println("451 Action requise abandonnée.\r");
      try {
    dataServer.close();
      } catch (IOException ex) { }
      try {
    dataSocket.close();
      } catch (IOException ex) { }
    }
  }
  /** Précise l'adresse Internet et le port TCP du client sur lequel
      doit se connecter le serveur pour transférer les données.
      @param param adresse Internet et port TCP
      dans un format particulier. */
  public void PORTHandler(String param) {
    try {
      extractDataInfo(param);
      output.println("200 PORT: succès de la requête.\r");
    } catch (UnknownHostException e) {
      output.println("500 'PORT " + param + "': requête incomprise.\r");
    }
  }
  /** Précise l'indice en octets où doit redémarrer le chargement
      @param param indice de démarrage. */
  public void RESTHandler(String param) {
    if (!login) {
      output.println("530 Logez vous avec USER et PASS.\r");
      return;
    }
    try {
      int tmp = Integer.parseInt(param);
      restartIndex = tmp;
      output.println("350 Indice de redémarrage à " + restartIndex +
             ". Utilisez STORE ou RETRIEVE pour un transfert.\r");
    } catch (NumberFormatException e) {
      output.println("500 'REST " + param + "': requête incomprise.\r");
    }
  }
  /** Initie la demande de transfert.
      @param param nom du fichier à transférer. */
  public void RETRHandler(String param) {
    if (!login) {
      output.println("530 Logez vous avec USER et PASS.\r");
      return;
    }
    if (type.compareTo("I")!=0) {
      output.println("504 Requête non implantée pour ce type.\r");
      return;
    }
    FileInputStream file = null;
    try {
      File f = new File(wdir,param);
      file = new FileInputStream(f);
      if (dataSocket==null) {
    dataSocket = new Socket();
    dataSocket.setReuseAddress(true);
    dataSocket.bind(serverDataAddress);
    dataSocket.connect(clientDataAddress);
      }
      output.println("150 Ouverture connexion en mode binaire pour "+
             param +" (" + file.available() +").\r");
    } catch (FileNotFoundException e) {
      output.println("550 "+ new File(wdir, param) +
             ": Aucun fichier ou répertoire de ce nom.\r");
      return;
    } catch (IOException e) {
      output.println("425 Impossible de créer la connexion de données\r");
      return;
    }
    try {
      send(file,dataSocket.getOutputStream());
      output.println("226 Transfert complet.\r");
    } catch (IOException e) {
      output.println("426 Connection de données interrompue.\r");
    } finally {
      try {
    file.close();
    if (!dataSocket.isClosed()) {
      dataSocket.close();
    }
      } catch (IOException e) { }
    }
  }
  /** Termine la session FTP.
      @param param argument de la requête ignoré. */
  public void QUITHandler(String param) {
    output.println("221 Au revoir\r");
    closed = true;
    if (dataSocket!=null && !dataSocket.isClosed())
      try {
    dataSocket.close();
      } catch (IOException e) { }
  }
  /** Vérification de mot de passe (ici retourne toujours vrai). */
  boolean checkPasswd(String user,String passwd) {
    return true;
  }
  /** Fontion permettant d'extraire l'adresse Internet A.B.C.D et
      le port TCP E*256+F d'une chaîne de caractères A,B,C,D,E,F,
      donnant l'adresse de la socket utilisée par le client pour
      le transfert. */
  void extractDataInfo(String param) throws UnknownHostException {
    StringBuffer inetAddress = new StringBuffer(15);
    int i=0;
    // Extraction de l'adresse IP
    for (int j=0; j<4 ; j++){
      for (int k=0; k<3 && param.charAt(i)!=','; k++,i++) {
    if (!Character.isDigit(param.charAt(i)))
      throw new IllegalArgumentException();
    inetAddress.append(param.charAt(i));
      }
      if (param.charAt(i)!=',') 
    throw new IllegalArgumentException();
      if (j!=3) 
    inetAddress.append('.');
      i++;
    }
    // Extraction du port TCP
    int port1=0;
    int port=0;
    for (int k=0; k<3 && param.charAt(i)!=','; k++,i++) {
      if (!Character.isDigit(param.charAt(i)))
    throw new IllegalArgumentException();
      port1 = port1*10+ param.charAt(i)-'0';
    }
    if (param.charAt(i)!=',') 
      throw new IllegalArgumentException();
    i++;
    port1 *= 256;
    for (int k=0; k<3 && i<param.length(); k++,i++) {
      if (!Character.isDigit(param.charAt(i))) 
    throw new IllegalArgumentException();
      port = port*10+param.charAt(i)-'0';
    }
    if (i!=param.length())
      throw new IllegalArgumentException();
    // Création de l'adresse de la socket cliente
    clientDataAddress =
      new InetSocketAddress(InetAddress.getByName(inetAddress.toString()),
                port1+port);
  }
  /** Retourne toutes les données lues sur le flot en lecture sur
      le flot en écriture.
      @param input flot en lecture
      @param output flot en écriture. */
  void send(InputStream input, OutputStream dataOutput)
    throws IOException {
    int nbRead=0;
    input.skip(restartIndex);
    while ((nbRead=input.read(buffer,0,buffer.length))!=-1)
      dataOutput.write(buffer,0,nbRead);
    restartIndex=0;
  }
}