package chat;



import cryptix.provider.Cryptix;

import cryptix.provider.rsa.RawRSAPrivateKey;

import cryptix.provider.rsa.RawRSAPublicKey;

import java.io.BufferedReader;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.FileInputStream;

import java.io.FileReader;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.net.DatagramPacket;

import java.net.SocketException;

import java.security.Key;

import java.security.KeyException;

import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.SecureRandom;

import java.security.Security;

import java.util.Vector;

import xjava.security.KeyGenerator;



/**
 * Classe che realizza il monitor di output della <i>chat</i>; in
 * questo contesto il termine "monitor" è da indendersi come "video",
 * "area di visualizzazione".
 * <p>
 *
 * @author    <em>Alessandro Gaspari</em>
 * @version   1.0
 */
public class Monitor extends SocketUDPaffidabile {

  /** Porta UDP di default in assenza di direttive in fase di costruzione. */
  public static final int PORTA_DEFAULT = 2001;

  /**
   * Nome del file di scambio dei messaggi con <code>Console</code>.
   *
   * @see   Console
   */
  public static final String FILE_SWAP = "swap.txt";

  /**
   * Nome del file ASCII contenente la rubrica delle conoscenze.
   *
   * @see   Database
   */
  public static final String FILE_DATABASE = "Database.txt";

  /** Parametro di configurazione per usare o meno la crittografia. */
  private static final boolean ACCLUDI_FIRMA_DIGITALE = true;

  /** Parametro di configurazione per impiegare o meno la firma digitale. */
  private static final boolean ADOTTA_CRITTOGRAFIA = true;


  /**
   * Un <i>wrapper</i> attorno al file coi messaggi provenienti dalla
   * <code>Console</code>.
   *
   * @see   Console
   */
  private BufferedReader swap;

  /** La rubrica con tutte le conoscenze. */
  private Database rubrica;

  /** L'identità da adottare durante la <i>chat</i>. */
  private String nickname;

  /** La chiave crittografica pubblica dell'utente. */
  private PublicKey c_pubblica;

  /** La chiave crittografica privata dell'utente. */
  private PrivateKey c_privata;

  /** Oggetto per generare e verificare le firme digitali. */
  private Firma md5rsa;

  /** Oggetto per applicare l'algoritmo crittografico RSA ai dati. */
  private Crittografia rsa;

  /** Il generatore delle chiavi segrete di sessione per IDEA. */
  private KeyGenerator generatore_c;

  /** La chiave segreta di sessione per IDEA. */
  private Key c_sessione;

  /** Oggetto per applicare l'algoritmo crittografico IDEA ai dati. */
  private Crittografia idea;

  /**
   * Lista degli utenti a cui sono stati iniviati uno o più
   * datagrammi di tipo SYN. Usato per decidere se accettare o meno
   * un OK ricevuto.
   *
   * @see   Protocollo#SYN
   * @see   Protocollo#OK
   */
  private Vector SYNinviati = new Vector();

  /**
   * Archivio degli utenti che ci hanno inviato un SYN ed ai quali
   * non abbiamo ancora inviato il corrispondente OK. Rappresenta
   * la lista dei SYN in attesa di risposta.
   *
   * @see   Protocollo#SYN
   * @see   Protocollo#OK
   */
  private Database SYNricevuti = new Database();

  /** Archivio con i <code>Contatto</code> dell'attuale gruppo di discussione. */
  private Database interlocutori = new Database();

  /** Un generatore di "casualità". */
  private SecureRandom entropia = new SecureRandom();

  /** Numero di sequenza dei datagrammi UDP inviati. */
  private int seq_num = Integer.MIN_VALUE + entropia.nextInt(0xffff);


  /**
   * Costruttore che avvia il <code>Monitor</code> sulla porta UDP
   * di default <code>PORTA_DEFAULT</code>. In caso di differenti
   * eccezioni termina il programma mediante <code>System.exit(0)</code>.
   *
   * @exception   SocketException   se generata dal costruttore della super-classe.
   * @see         #PORTA_DEFAULT
   * @see         SocketUDPaffidabile
   * @see         SocketUDPaffidabile#SocketUDPaffidabile(int)
   */
  Monitor() throws SocketException {
    this(PORTA_DEFAULT);
  }

  /**
   * Costruttore che avvia il <code>Monitor</code> sulla porta UDP
   * indicata. In caso di differenti eccezioni termina il programma
   * mediante <code>System.exit(0)</code>.
   *
   * @param       porta             la porta UDP da usare per il <i>socket reliable</i>.
   * @exception   SocketException   se generata dal costruttore della super-classe.
   * @see         SocketUDPaffidabile
   * @see         SocketUDPaffidabile#SocketUDPaffidabile(int)
   */
  Monitor(int porta) throws SocketException {
    super(porta);
    try {
      System.out.println("Monitor sulla porta UDP " + porta + ".");
      swap    = new BufferedReader(new FileReader(FILE_SWAP));
      rubrica = new Database(FILE_DATABASE);
      System.out.print("Lettura del nickname in corso...");
      nickname = swap.readLine();
      System.out.println("fatto.\nBenvenuto \"" + nickname + "\"!");
      c_pubblica   = new RawRSAPublicKey(new FileInputStream(nickname + ".pub"));
      c_privata    = new RawRSAPrivateKey(new FileInputStream(nickname + ".pri"));
      md5rsa       = new Firma("MD5/RSA");
      rsa          = new Crittografia("RSA/ECB/PKCS#7");
      generatore_c = KeyGenerator.getInstance("IDEA");
      generatore_c.initialize(entropia);
      idea         = new Crittografia("IDEA/ECB/PKCS#5");
    } catch (Exception e) {
        e.printStackTrace();
        System.exit(0);
      }
    rubrica.cancella(nickname);   // User non nella rubrica!

  }

  /**
   * <b>Procedura</b> che numera i datagrammi UDP prima di inviarli
   * in rete; utilizza <code>seq_num</code> incrementandolo di una
   * quantità casuale ogni volta, lo firma se la costante di classe
   * <code>ACCLUDI_FIRMA_DIGITALE</code> vale <code>true</code>,
   * infine lo cifra se </code>ADOTTA_CRITTOGRAFIA == true</code>.
   *
   * @param    d   il <code>DatagramPacket</code> da numerare.
   * @return   il <i>sequence number</i> aggiunto a <code>d</code>.
   * @see      #seq_num
   * @see      #ACCLUDI_FIRMA_DIGITALE
   * @see      #ADOTTA_CRITTOGRAFIA
   * @see      SocketUDPaffidabile#inserisciSeqNum(DatagramPacket)
   */
  protected int inserisciSeqNum(DatagramPacket d) {
    Messaggio m = messaggioFromDatagram(d);
    seq_num += entropia.nextInt(0xffff) + 1;
    if (ACCLUDI_FIRMA_DIGITALE)
      m = new Messaggio(m.daChi(), m.aChi(), seq_num, m.tipo(), m.testo(), md5rsa.firma(m.testo(), c_privata));
    else
      m.scriviSeqNum(seq_num);
    if (ADOTTA_CRITTOGRAFIA)
      try {
        m = new Messaggio(m.daChi(), m.aChi(),
                          interlocutori.contiene(m.aChi()) ? idea.cifra(m.cheCosa(), c_sessione)
                                                           : rsa.cifra(m.cheCosa(), rubrica.seleziona(m.aChi()).chiave()));
        
      } catch (KeyException e) {
          e.printStackTrace();
        }
    messaggioToDatagram(m, d);
    return seq_num;
  }

  /**
   * <b>Procedura</b> che ricava il numero di sequenza dei datagrammi
   * UDP appenda ricevuti via <i>socket</i>.
   * Decifra il <code>DatagramPacket</code> se la costante di classe
   * </code>ADOTTA_CRITTOGRAFIA</code> vale <code>true</code>,
   * scegliendo la chiave e l'algoritmo di decifratura sulla base
   * del mittente del datagramma e sulla lista degli interlocutori.
   *
   * @param    d   il <code>DatagramPacket</code> da manipolare.
   * @return   il <i>sequence number</i> letto da <code>d</code>.
   * @see      #ADOTTA_CRITTOGRAFIA
   * @see      #interlocutori
   * @see      SocketUDPaffidabile#estraiSeqNum(DatagramPacket)
   */
  protected int estraiSeqNum(DatagramPacket d) {
    Messaggio m = messaggioFromDatagram(d);
    if (ADOTTA_CRITTOGRAFIA)
      try {
        m = new Messaggio(m.daChi(), m.aChi(),
                          interlocutori.contiene(m.daChi()) ? idea.decifra(m.cheCosa(), c_sessione)
                                                            : rsa.decifra(m.cheCosa(), c_privata));
        messaggioToDatagram(m, d);
      } catch (KeyException e) {
          e.printStackTrace();
        }
    return m.seqNum();
  }

  /**
   * Metodo che serializza un <code>Messaggio</code> riversandone
   * il contenuto entro un <code>DatagramPacket</code>.
   *
   * @param   m   il messaggio da serializzare.
   * @param   d   il datagramma UDP entro cui scrivere la versione
   *              serializzata di <code>m</code>.
   * @see     Messaggio
   */
  void messaggioToDatagram(Messaggio m, DatagramPacket d) {
    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream    oos  = new ObjectOutputStream(baos);
      oos.writeObject(m);
      oos.flush();
      byte[] buf = baos.toByteArray();
      oos.close();
      d.setData(buf, 0, buf.length);
    } catch (Exception e) {
        e.printStackTrace();
      }
  }

  /**
   * Metodo che estrae un <code>Messaggio</code> serializzato da un
   * <code>DatagramPacket</code>. Compie l'azione complementare di
   * <code>messaggioToDatagram(Messaggio, DatagramPacket)</code>.
   *
   * @param    d   il datagramma da cui leggere la versione
   *               serializzata di un <code>Messaggio</code>.
   * @return   il messaggio estratto da <code>d</code>, oppure
   *           <code>null</code> in caso di errore.
   * @see      #messaggioToDatagram(Messaggio, DatagramPacket)
   * @see      Messaggio
   */
  Messaggio messaggioFromDatagram(DatagramPacket d) {
    try {
      ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(d.getData(), d.getOffset(), d.getLength()));
      Messaggio m = (Messaggio) ois.readObject();
      ois.close();
      return m;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
      }
  }

  /**
   * <i>Main-loop</i> del programma: legge il contenuto del file di
   * scambio con la <code>Console</code> e processa il comando,
   * quindi esamina anche il <i>socket</i> affidabile per verificare
   * i <code>Messaggio</code> ricevuti.
   *
   * @see   #swap
   * @see   Console
   */
  void esegui() {
    byte[]         buf = new byte[0xFFFF - 8];
    DatagramPacket d   = new DatagramPacket(buf, buf.length);

    Vector cmd_uscita = new Vector();   // I 4 comandi per terminare

    cmd_uscita.add("bye");
    cmd_uscita.add("exit");
    cmd_uscita.add("quit");
    cmd_uscita.add("stop");

    while (true)   // Main-loop

      try {
        if (swap.ready()) {   // Input disponibile dal file di scambio?

          String cmd = swap.readLine().trim();
          if (cmd_uscita.contains(cmd))
            break;
          processaComando(cmd);
        }
        if (disponibili() == 0)   // Ci sono datagrammi disponibili?

          continue;
        d.setData(buf, 0, buf.length);
        ricevi(d);
        Messaggio m = messaggioFromDatagram(d);
        if (!m.aChi().equals(nickname))   // E' per noi?

          continue;
        Endpoint da_dove = new Endpoint(d.getAddress(), d.getPort());
        if (ACCLUDI_FIRMA_DIGITALE)   // Verifica della firma!

          if (!md5rsa.firmaOk(m.testo(), m.firma(), (PublicKey) rubrica.seleziona(m.daChi()).chiave())) {
            System.out.println(" !!!FIRMA NON VALIDA DA " + m.daChi() + "@" + da_dove + "!!!");
            continue;
          }
        switch (m.tipo()) {
          case Protocollo.SYN:  System.out.println(" <<<SYN DA " + m.daChi() + "@" + da_dove + ">>>");
                                SYNricevuti.modifica(new Contatto(m.daChi(), rubrica.seleziona(m.daChi()).chiave(), da_dove));
                                break;
          case Protocollo.OK:   if (SYNinviati.contains(m.daChi())) {
                                  System.out.println(" <<<OK DA " + m.daChi() + "@" + da_dove + ">>>");
                                  if (interlocutori.quanti() > 0)
                                    System.out.println(" Impossibile accettare \"" + m.daChi() + "\" a dialogo già in corso!");
                                  else {
                                    SYNinviati.remove(m.daChi());
                                    interlocutori.modifica(new Contatto(m.daChi(), rubrica.seleziona(m.daChi()).chiave(), da_dove));
                                    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(m.testo()));
                                    c_sessione = (Key) ois.readObject();
                                    while (ois.available() > 0)
                                      interlocutori.modifica((Contatto) ois.readObject());
                                    ois.close();
                                    visualizzaInterlocutori();
                                  }
                                }
                                break;
          case Protocollo.ADD:  if (interlocutori.contiene(m.daChi())) {
                                  ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(m.testo()));
                                  while (ois.available() > 0)
                                    interlocutori.modifica((Contatto) ois.readObject());
                                  ois.close();
                                  visualizzaInterlocutori();
                                }
                                break;
          case Protocollo.TEXT: if (interlocutori.contiene(m.daChi()))
                                  System.out.println("[" + m.daChi() + "] " + new String(m.testo()));
                                break;
          case Protocollo.FIN:  System.out.println(" <<<FIN DA " + m.daChi() + "@" + da_dove + ">>>");
                                interlocutori.cancella(m.daChi());
                                break;
          default:              System.out.println(" ???MESSAGGIO ILLEGALE DI TIPO 0x" + Utili.hexByte(m.tipo()) + " DA " + m.daChi() + "@" + da_dove + "???");
        }   // switch (m.tipo())

      } catch (Exception e) {
          e.printStackTrace();
        }
    try {       // Chiusura del file di scambio con la Console

      swap.close();
    } catch (IOException e) {}
    try {       // Chiusura del dialogo in corso, avvisando il gruppo

      agliInterlocutori(Protocollo.FIN, null);
    } catch (IOException e) {}
    chiudi();   // Chiusura del socket UDP affidabile

  }

  /**
   * Interpreta un comando letto dal file di scambio <code>swap</code>
   * con la <code>Console</code>. Non termina il programma perché non
   * riconosce i comandi di uscita.
   *
   * @param       cmd           la linea letta da <code>swap</code>.
   * @exception   IOException   causa le azioni che hanno a che
   *                            vedere con il <i>socket</i> interno.
   * @see         Console
   */
  void processaComando(String cmd) throws IOException {
    cmd = cmd.trim();
    if (cmd.length() == 0)
      return;
    if (cmd.equals("."))   // Chiudere il dialogo in corso

      if (interlocutori.quanti() == 0)
        System.out.println(" Nessun dialogo in corso.");
      else
        try {
          System.out.println(" Chiusi regolarmente " + agliInterlocutori(Protocollo.FIN, null) + " colloqui su " + interlocutori.quanti() + ".");
          interlocutori.cancella();
        } catch (IOException e) {
            e.printStackTrace();
          }
    else   // cmd != "."

      if (cmd.startsWith(">") || cmd.startsWith("<")) {
        String chi = cmd.substring(1).trim();
        if (chi.length() == 0)
          visualizzaInterlocutori();
        else   // cmd != "." && chi != ""

          if (interlocutori.contiene(chi))
            System.out.println(" \"" + chi + "\" è già un interlocutore.");
          else
            if (cmd.startsWith("<"))
              if (SYNricevuti.contiene(chi)) {   // Inviargli un OK

                if (interlocutori.quanti() == 0)
                  c_sessione = generatore_c.generateKey();

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream    oos  = new ObjectOutputStream(baos);
                oos.writeObject(c_sessione);
                Contatto[] v = interlocutori.tutti();
                for (int i = 0; i < v.length; ++i)
                  oos.writeObject(v[i]);
                oos.flush();
                byte[] buf = baos.toByteArray();
                oos.close();
                Contatto c = SYNricevuti.seleziona(chi);
                DatagramPacket d = new DatagramPacket(new byte[0], 0, ((Endpoint) c.dove().get(0)).IP(), ((Endpoint) c.dove().get(0)).porta());
                messaggioToDatagram(new Messaggio(nickname, chi, Protocollo.OK, buf), d);
                if (invia(d) == 1) {
                  System.out.println(" OK consegnato a \"" + chi + "\".");
                  if (interlocutori.quanti() > 0) {   // Avvisare il gruppo del nuovo ingresso

                    baos = new ByteArrayOutputStream();
                    oos  = new ObjectOutputStream(baos);
                    oos.writeObject(c);
                    oos.flush();
                    buf = baos.toByteArray();
                    oos.close();
                    System.out.println(" Avvisati " + agliInterlocutori(Protocollo.ADD, buf) + " interlocutori su " + interlocutori.quanti() + ".");
                  }
                  interlocutori.modifica(c);
                  SYNricevuti.cancella(chi);
                }
                else
                  System.out.println(" Impossibile consegnare l'OK a \"" + chi + "\".");
              }
              else
                System.out.println(" Nessun SYN ricevuto da \"" + chi + "\".");
            else   // cmd != "." && chi != "" && chi.startsWith(">")

              if (rubrica.contiene(chi))   // Consegnargli un SYN

                try {
                  Vector           dove = rubrica.seleziona(chi).dove();
                  DatagramPacket[] d    = new DatagramPacket[dove.size()];

                  for (int i = 0; i < d.length; ++i)
                    messaggioToDatagram(new Messaggio(nickname, chi, Protocollo.SYN, null), d[i] = new DatagramPacket(new byte[0], 0, ((Endpoint) dove.get(i)).IP(), ((Endpoint) dove.get(i)).porta()));
                  System.out.println(" Recapitati " + invia(d) + " SYN su " + d.length + " per \"" + chi + "\".");
                  if (!SYNinviati.contains(chi))
                    SYNinviati.add(chi);
                } catch (IOException e) {
                    e.printStackTrace();
                  }
              else
                System.out.println(" \"" + chi + "\" sconosciuto.");
      }
      else   // cmd != "." && cmd.charAt(0) != '>' && cmd.charAt(0) != '<'

        if (interlocutori.quanti() == 0)
          System.out.println(" Nessun dialogo in corso.");
        else
          try {
            System.out.println("[" + nickname + "] " + cmd + " {" + agliInterlocutori(Protocollo.TEXT, cmd.getBytes()) + "/" + interlocutori.quanti() + "}");
          } catch (IOException e) {
              e.printStackTrace();
            }
  }

  /**
   * <b>Funzione</b> che visualizza sullo <i>standard output</i>
   * i nomi degli interlocutori attuali.
   *
   * @see   #interlocutori
   */
  void visualizzaInterlocutori() {
    System.out.println(" Interlocutori:\n ==============\n\n");
    Contatto[] v = interlocutori.tutti();
    for (int i = 0; i < v.length; ++i)
      System.out.println(" " + v[i].nome());
  }

  /**
   * Metodo che invia il messaggio specificato a tutti gli utenti
   * dell'attuale gruppo di discussione.
   *
   * @param       tipo   il "tipo" di <code>Messaggio</code> da inviare.
   * @param       cosa   il testo da includere nel <code>Messaggio</code>.
   * @exception   se generata inviando il/i datagramma/i in rete.
   * @return      il numero di consegne confermate tramite ACK.
   * @see         Protocollo
   */
  int agliInterlocutori(byte tipo, byte[] cosa) throws IOException {
    if (interlocutori.quanti() == 0)
      return 0;
    DatagramPacket d[]   = new DatagramPacket[interlocutori.quanti()];
    Contatto[]     a_chi = interlocutori.tutti();
    for (int i = 0; i < d.length; ++i)
      messaggioToDatagram(new Messaggio(nickname, a_chi[i].nome(), tipo, cosa), d[i] = new DatagramPacket(new byte[0], 0, ((Endpoint) a_chi[i].dove().get(0)).IP(), ((Endpoint) a_chi[i].dove().get(0)).porta()));
    return invia(d);
  }


  /**
   * L'<i>entry-point</i> del programma. Installa dinamicamente la
   * libreria <code>Cryptix</code> ed avvia un'istanza di
   * <code>Monitor</code>, assegnandole la porta UDP eventualmente
   * specificata tramite la linea di comando.
   *
   * @param   args   gli argomenti della linea di comando.
   */
  public static void main(String[] args) {
    Security.addProvider(new Cryptix());   // Installa dinamicamente Cryptix

    try {
      switch (args.length) {
        case 0:  new Monitor().esegui();
                 break;
        case 1:  new Monitor(Integer.parseInt(args[0])).esegui();
                 break;
        default: System.out.println("Sintassi d'uso:   java chat.Monitor [porta UDP]");
      }
    } catch (SocketException e) {
        e.printStackTrace();
      }
  }
}


syntax highlighted by Code2HTML, v. 0.8.11