TFTPServer.java

package fr.umlv.ji.udp;
import java.net.*;
import java.io.*;
import java.util.prefs.*;
import java.util.logging.*;

/** Classe des exceptions utilisées pour le serveur TFTP. */
class TFTPException extends Exception {}

/** Classe du serveur de fichier selon le protocole TFTP (RFC 1350). */
public class TFTPServer {
  // Récupération du journaliseur et des préférences utilisateur
  final static Logger logger 
        = Logger.getLogger("fr.umlv.ji.udp");  
  final static Preferences prf
        = Preferences.userNodeForPackage(fr.umlv.ji.udp.TFTPServer.class);

  /* Constantes spécifiques du protocole TFTP (RFC 1350). */
  // Codes des opérations TFTP
  final static byte RRQ_OPCODE   = 1; // Requête lecture
  final static byte WRQ_OPCODE   = 2; // Requête écriture
  final static byte DATA_OPCODE  = 3; // Transmission
  final static byte ACK_OPCODE   = 4; // Acquittement
  final static byte ERROR_OPCODE = 5; // Erreur
  // Codes des erreurs TFTP
  final static byte NOTDEF_ERROR           = 0;
  final static byte FILE_NOT_FOUND_ERROR   = 1;
  final static byte ACCESS_VIOLATION_ERROR = 2;
  // Taille des entêtes TFTP
  final static int HEADER_SIZE = 4;
  // Constante pour le mode de transfert
  final static String BINARY_MODE = "octet";
  // Taille maximale d'un bloc de données
  final static int BLOCK_SIZE = 512;

  /* Constantes paramétrables (port par défaut, délai d'attente et
     nombre de retransmissions). */
  final static int TFTP_PORT     = prf.getInt("TFTP_PORT",69);
  final static int RETRY_TIMEOUT = prf.getInt("RETRY_TIMEOUT",1000);
  final static int RETRY_NB      = prf.getInt("RETRY_NB",2);
  // Plus grand entier codable sur deux octets (numéros de blocs)
  final static int USHORT_MAX
    = prf.getInt("USHORT_MAX",Short.MAX_VALUE-Short.MIN_VALUE);

  /* Variables associées à un serveur TFTP. */
  private DatagramSocket socket;    // pour l'attente des requêtes
  private DatagramSocket tidSocket; // pour les transferts de fichiers
  private DatagramPacket packet;    // commun à toutes les communications 
  private byte[] buf;               // stockage requêtes et données
  private byte[] bufAck;            // stockage des acquittements
  private String fileName;          // fichier à transférer
  private int blockNb;              // numéro de bloc
  
  /** Construit un serveur attaché au port UDP par défaut. */
  public TFTPServer() throws SocketException {
    this(TFTP_PORT);
  }
  /**
   * Construit un serveur attaché à un port UDP donné. Initialise
   * la socket locale, les zones de stockage et le datagramme.
   */
  public TFTPServer(int port) throws SocketException {
    socket = new DatagramSocket(port);
    buf = new byte [BLOCK_SIZE+HEADER_SIZE];
    bufAck = new byte [HEADER_SIZE];
    packet = new DatagramPacket(buf,0,buf.length);
  }
  /**
   * Attend les requêtes TFTP sur le port UDP. Dès qu'un datagramme
   * est reçu, sa requête est prise en compte et donne lieu, si elle
   * est valide, à l'établissement d'une pseudo-connexion entre le
   * client et un TID choisi par le serveur.
   */
  public void run() {
    logger.info("Serveur démarré, attaché à " +
        socket.getLocalSocketAddress());
    try {
      while (true) {
    // Attente de requête: mise en place de la zone de réception
    packet.setData(buf,0,buf.length);
    socket.receive(packet);
    logger.fine("Requête reçue de " + packet.getSocketAddress());
    // Avant que la pseudo-connexion soit établie,
    // tidSocket vaut par défaut socket (pour sendError())
    tidSocket = socket;
    // Prise en compte de la requête du datagramme reçu
    analyseRequest();
      }
    } catch (IOException ioe) {
      logger.log(Level.SEVERE,
         "Problème d'entrée/sortie sur la socket locale",ioe);
    }
  }
  /** Prend en compte un datagramme de requête reçu par le serveur. */
  private void analyseRequest() {
    /* Le premier octet du code d'opération doit être nul. */
    if (buf[0] != 0) {
      logger.fine("Code d'opération invalide");
      sendError(NOTDEF_ERROR,"Code d'opération invalide");
      return;
    }
    /* Traitement des requêtes en fonction du second octet du code
       d'opération (on ignore les datagrammes de code non attendu). */
    switch (buf[1]) {
    case DATA_OPCODE :   // Transmission de données: ignorée
    case ACK_OPCODE :    // Acquitement: ignoré
    case ERROR_OPCODE :  // Erreur: ignorée
      break;
    case WRQ_OPCODE :    // Demande d'écriture: non implantée
      logger.fine("Code d'opération non implantée");
      sendError(NOTDEF_ERROR,"Opération " + buf[1] + " non implantée");
      break;
    case RRQ_OPCODE :    // Demande de lecture: traitement de la requête
      logger.fine("Code d'opération de lecture");
      processRead();
      break;
    default :            // Code inconnu: envoi d'un message d'erreur
      logger.fine("Code d'opération invalide");
      sendError(NOTDEF_ERROR,"Opération invalide");
      break;
    }  
  }
  /** Envoie un message d'erreur TFTP de type errorType avec un message
      à l'émetteur de la requête. */
  private void sendError(int errorType, String msg) {
    try {
      // Placement du code d'opération 
      buf[0] = 0; buf[1] = ERROR_OPCODE;
      // Placement du code d'erreur
      buf[2] = 0; buf[3] = (byte) errorType;
      // Remplissage de la zone de stockage avec le message
      byte[] msgAscii = msg.getBytes("ASCII");
      System.arraycopy(msgAscii,0,buf,HEADER_SIZE,msgAscii.length);
      // Ajout du dernier octet à zéro
      buf[HEADER_SIZE+msgAscii.length] = 0;
      // Spécification des données dans le datagramme
      packet.setData(buf,0,HEADER_SIZE+msgAscii.length+1);
      // Envoi du datagramme
      tidSocket.send(packet);
    } catch(IOException ioe) {
      logger.log(Level.WARNING,
         "Problème d'entrée/sortie. Transfert Interrompu",ioe);
    }
  }
  /** Traitement d'une requête de lecture. */
  private void processRead() {
    // Récupération du nom de fichier dans le datagramme reçu
    fileName = extractString(buf,2);
    // Récupération du mode de transfert
    String mode = extractString(buf,3+fileName.length());
    logger.fine("Fichier demandé : " + fileName + " en mode " + mode);
    // Si mode différent de "octet", envoie une erreur et ignore requête
    if (mode.compareToIgnoreCase(BINARY_MODE) != 0) {
      logger.fine("Mode de transfert non implanté");
      sendError(NOTDEF_ERROR,"Mode " + mode + " non implanté");
      return;
    }
    sendFile(); // Envoie le fichier requis
  }
  /** Construit un objet String à partir d'un tableau d'octets.
      La chaîne de caractères débute à un index particulier
      du tableau et se termine par un octet à 0.  */
  private String extractString(byte[] buffer, int offset) {
    int i;
    String s = null;
    for (i=offset; buffer[i]!=0; i++) ;
    try { 
      s = new String(buffer,offset,i-offset,"ASCII");
    } catch (UnsupportedEncodingException e) {}
    return s;
  }
  /** Envoie un datagramme de données et attend son acquittement
      pendant RETRY_TIMEOUT millisecondes. Réessae RETRY_NB fois, puis
      lève une exception si un acquittement correct n'est pas reçu. */
  private void sendData(byte[] buffer, int length)
    throws IOException, TFTPException {
    for (int i=0; i<RETRY_NB; ) {
      // Spécification des données dans le datagramme et émission
      packet.setData(buffer,0,length);
      tidSocket.send(packet);
      // Configuration du datagramme pour réception de l'acquittement
      packet.setData(bufAck,0,bufAck.length);
      try {
    tidSocket.receive(packet);
      } catch (SocketTimeoutException ste) {
    logger.fine("Délai d'acquittement dépassé.");
    i++; // Si le délai est écoulé (pas d'acquittement) on réémet
    continue;
      }
      // Teste si le datagramme reçu contient le bon acquittement
      if (acknowledge(bufAck, buffer))
    return;
      
      // Sinon, on lève une exception si le code du datagramme
      // reçu indique une erreur sur le serveur ou sur le client
      if (bufAck[0] == 0 &&
      bufAck[1] == ERROR_OPCODE) {
    logger.warning("Acquittement d'erreur client ou serveur");
    sendError(NOTDEF_ERROR,"Transfert interrompu");
    throw new TFTPException();
      }
    }
    // Si RETRY_NB essais n'ont pas suffi, lève une exception
    logger.warning("Nombre de tentatives de retransmission dépassé");
    sendError(NOTDEF_ERROR,"Transfert interrompu");
    throw new TFTPException();
  }
  /** Teste si l'acquittement correspond bien aux données envoyées. */
  private boolean acknowledge(byte[] ack, byte[]data) {
    return (ack[0] == 0 &&
        ack[1] == ACK_OPCODE &&
        ack[2] == data[2] &&
        ack[3] == data[3]);
  }
  /** Retourne un numéro de port aléatoire. Le port retourné est non
      réservé, c'est-à-dire entre 1024 et 65535.  */
  private int getRandomPort() {
    // Par simplicité, retourne un port entre 1024 et 61024
    return Math.round((float)(Math.random()*60000)) + 1024;
  }
  /** Envoie le fichier spécifié par fileName, par portions de
      BLOCK_SIZE octets. Pour le transfert, une pseudo-connexion est
      établie entre une socket locale (numéro de port aléatoire, TID)
      et la socket du client. */
  private void sendFile() {
    try {
      /* Vérifie que le fichier existe et est lisible. */
      FileInputStream input;
      try {
    // Ouverture du fichier
    input = new FileInputStream(fileName);
      } catch(FileNotFoundException fnfe){
    logger.log(Level.FINE,"Fichier demandé introuvable",fnfe);
    sendError(FILE_NOT_FOUND_ERROR,"Fichier introuvable");
    throw new TFTPException();
      } catch (SecurityException se) {
    logger.log(Level.FINE,"Accès refusé au fichier demandé",se);
    sendError(ACCESS_VIOLATION_ERROR,"Accès refusé");
    throw new TFTPException();
      }   
      /* Crée une socket locale et établit la pseudo-connexion. */
      while (true) {
    int tid = getRandomPort();
    try {
      // Tentative d'attachement local à un port aléatoire
      tidSocket = new DatagramSocket(tid);
    } catch (SocketException se) {
      logger.fine("Échec d'attachement local au port " + tid);
      continue; // si le port n'est pas libre, on essaye avec un autre
    }
    tidSocket.connect(packet.getSocketAddress());
    break;      // si la pseudo-connexion réussit, on passe à la suite
      }
      /* La pseudo-connexion est établie. */
      logger.info("Pseudo-connection établie entre\n\t" +
          tidSocket.getLocalSocketAddress() + " et " +
          tidSocket.getRemoteSocketAddress());
      
      /* Transfère le fichier par blocs. */   
      tidSocket.setSoTimeout(RETRY_TIMEOUT);  // Délai maximum
      blockNb = 1;                            // Numéro de séquence
      buf[0] = 0;
      buf[1] = DATA_OPCODE;                   // Code opération
      int nbRead;                             // Blocs de données
      do {
    try {
      nbRead = input.read(buf,4,BLOCK_SIZE);
    } catch (IOException ioe) {
      logger.log(Level.FINE,"Problème d'accès au fichier",ioe);
      sendError(NOTDEF_ERROR,"Transfert interrompu");
      throw new TFTPException();
    }
    if (nbRead == -1)
      nbRead = 0;
    buf[2] = (byte)(blockNb>>8);
    buf[3] = (byte)(blockNb);
    sendData(buf,HEADER_SIZE+nbRead);
    blockNb = (blockNb+1)%USHORT_MAX;
      } while (nbRead == BLOCK_SIZE);
      logger.fine("Transfert effectué avec succès");
    } catch (PortUnreachableException pe) {
      logger.log(Level.WARNING,"Socket pseudo-connectée injoignable", pe);
    } catch (IOException ioe) {
      logger.log(Level.WARNING,"Problème d'entrée/sortie", ioe);
    } catch (TFTPException tftpe) {
      // Un message d'erreur a déjà été envoyé
      logger.fine("Transfert interrompu");
    } finally {
      logger.info("Fermeture de la pseudo-connection (éventuelle)");
      tidSocket.disconnect();
      tidSocket = null;
    }
  }
  /** Méthode appelée à la destruction de l'objet serveur. */ 
  protected void finalize() {
    socket.close();
  }
  /**
   * Méthode principale du serveur qui lance un serveur TFTP
   * dans le processus léger initial. Accepte un numéro de port. 
   */
  public static void main(String[] args) throws SocketException {
    TFTPServer server;
    // Création du serveur
    if (args.length > 0 )
      server = new TFTPServer(Integer.parseInt(args[0]));
    else
      server = new TFTPServer();
    // Démarrage du serveur.
    server.run();
  }
}