MokaByte Numero 24  -  Novembre 98
Caffé e Prezzemolo
di 
Piergiuseppe
Spinelli 
Come usare Java per scrivere programmi di comunicazione...e vivere felici!


Ai nostri giorni, specialmente con le nuove leggi sulla privacy, appare assai scorretto introdursi furtivamente nelle conversazioni altrui, per spiare, controllare, annotare…  Ciò non è sempre vero: esistono validi motivi per intromettersi nelle comunicazioni tra un server ed i suoi client; eccone alcuni: 1.  estendere i controlli di accesso (o addirittura fungere da firewall)  2. estendere le capacità di log del server     3.registrare annotazioni in formati indipendenti dal particolare server  4. conservare le informazioni più frequentemente accedute per accelerarne il rilascio.



 
 

Proxy generalizzato

Viene spesso chiamato proxy un programma che intercetti una comunicazione client/server in modo trasparente ad entrambe le parti redirigendo rispettivamente l’input di ogni lato sull’output dell’altro. Il nome è dovuto al fatto che tale oggetto viene aggiunto in prossimità di uno dei due device comunicanti, ad esempio su un server della rete locale dotato di un collegamento modem con un internet provider: in questo modo i vari client si connettono via LAN e ricevono le informazioni richieste senza sapere quando il proxy le preleva da internet o dalla propria cache. Secondo il linguaggio dei pattern, l'intento di un oggetto proxy è quello di "fornire un surrogato o segnaposto per un altro oggetto per controllare l'accesso ad esso" (vedi http://st-www.cs.uiuc.edu/cgi-bin/wikic/wikic?ProxyPattern).

Ad un vecchio programmatore come me, scrivere un programma proxy può apparire, istintivamente, un compito arduo: sono passati appena pochi anni da quando si realizzavano applicazioni client/server scambiando file (di richiesta e risposta) via FTP per evitare la complessità della programmazione di rete (poco conosciuta, poco documentata, facilmente preda di problemi di condivisione, difficilissima da implementare senza ricorrere a processi concorrenti, ai tempi in cui si parlava di thread, o processi leggeri, con reverenziale timore). Appare quindi quasi miracolosa la semplicità con cui è possibile metter su l’ossatura di un programma proxy in Java. Esistono almeno quattro caratteristiche del linguaggio di SUN che possono aiutarci al riguardo:

Nessuna delle suelencate proprietà appartiene unicamente a Java e, ormai, quasi tutti i linguaggi hanno arricchito il proprio bagaglio di simili possibilità. Tuttavia…

Prendiamo ad esempio il VisualBasic 5: è adesso possibile scrivere oggetti ActiveX multithreaded, accedere a classi di comunicazione, caricare dinamicamente controlli OLE. Eppure sfido chiunque a scrivere in VB un programma di comunicazione con la stessa semplicità ed eleganza resa possibile da Java. Si tratta in fondo di un problema generazionale. I primi linguaggi di programmazione esprimevano tutta la loro potenza nel calcolo. I loro immediati successori si sbizzarrivano nella manipolazione dei dati. Quelli della generazione visuale curavano il look innanzi tutto mentre i linguaggi contemporanei sono dei tipi particolarmente comunicativi.

Chi ha detto "Il computer è la rete"?

Petrosino tesse la sua rete

Ho chiamato Petrosino questo piccolo scheleton di proxy perché è pensato per mettersi in mezzo un po’ a tutto (almeno nella famiglia di protocolli TCP/IP) , proprio come il prezzemolo e l’omonimo celeberrimo poliziotto.

Si tratta di un’applicazione stand-alone che, in configurazione base, non fa assolutamente nulla (tranne rallentare la comunicazione….) offrendo però una piattaforma per costruire semplici programmi di filtro o di log per i vari protocolli. Ad esempio, digitando:

>java Petrosino 25 mailserver.com 25 1 filtro1 filtro2 …
attiviamo sulla porta 25 del nostro computer un proxy per il server mailserver.com (sempre sulla porta 25) caricando una lista di moduli filtro che leggono ogni riga scambiata tra client e sever per svolgere azioni custom. E' possibile far coesistere sullo stesso computer il server ed il proxy, naturalmente su porte diverse:
>java Petrosino 8080 localhost 80 1 HttpFilter
Descrizione delle classi

La classe principale, Petrosino, apre un server socket sulla porta indicata dal primo parametro e resta in attesa di connessioni da parte dei client (il limite consiste nel poter monitorare solo sessioni aperte dai client, come avviene per i protocolli HTTP o SMTP, ma questo va bene per la maggior parte delle situazioni). Provvede inoltre al caricamento dinamico delle classi filtro previa controllo che esse implementino l’interfaccia Petrosino.Listener.

Se il quarto parametro è diverso da 0, Petrosino visualizzerà sul video (monitor Java) tutte le stringhe che passano nei due sensi della connessione. Ogni volta che un client ignaro si connette (convinto di parlare direttamente col server), il proxy crea un istanza della classe Petrosino.Twins che a sua volta apre una connessione socket con il server sulla porta specificata (parametri 2 e 3) ed i rispettivi stream di I/O sia parte client che server.

Il nome Twins è dovuto al fatto che tale istanza, una volta aperti tutti i canali di comunicazione, provvede a creare due thread simmetrici di tipo Petrosino.Twins.ConnectionTask. La simmetria sta nel fatto che un’istanza riceve l’input stream del client e l’output del server mentre l’altra resta in attesa di dati dal server e scrive sul client.

Ogni volta che viene ricevuta una stringa da uno dei due estremi della comunicazione, il gemello delegato al suo monitoraggio la passa iterativamente a tutti i filtri caricati e controlla il flag da essi ritornato per verificare se continuare il reindirizzamento del canale verso l'altro capo o se, addiritttura, interrompere la comunicazione. Ogni oggetto di tipo ConnectionTask è quindi sempre abbinato ad un gemello simmetrico con il quale condivide i dati dell’outer class Twins. In coda all'articolo vedremo di approfondire questa tecnica che ho chiamato condivisione controllata.

Il codice di Petrosino
 
    import java.io.*;

    import java.net.*;

    import java.util.*;

    public class Petrosino implements Runnable{

        public static final int CLIENT=0;

        public static final int SERVER=1;

        public static final int OPEN=0;

        public static final int CLOSE=1;

        public static final int STOP=2;

        private Thread waitTask=null;

        private ServerSocket ss;

        private int myPort=1080;

        private String sAddr="127.0.0.1";

        private int sPort=80;

        private boolean srFlag=false;

        private Vector classi=new Vector();

        public static void main(String[] pars){

            Petrosino p = new Petrosino(pars);

        }

        private Petrosino(String[] pars){

            try{

                myPort = Integer.parseInt(pars[0]);

                sAddr = pars[1];

                sPort = Integer.parseInt(pars[2]);

                srFlag = (Integer.parseInt(pars[3])!=0);

                ss = new ServerSocket(myPort);

                for(int ind=4;ind<pars.length;ind++){

                    String className=pars[ind];

                    Class c = Class.forName(className);

                    Object obj=c.newInstance();

                    if(obj instanceof Listener){

                        classi.addElement(c);

                        System.out.println("Filter '" + c.getName() + "' Loaded");

                        ((Listener)obj).cleanUp();

                    }else{

                        System.out.println("Filter '" + c.getName() + "' is not a Petrosino.Listener");

                    }

                }

            }catch(Exception e){

                e.printStackTrace();

                return;

            }

            waitTask = new Thread(this);

            waitTask.start();

            System.out.println("Petrosino activated on port " + myPort + " for " + sAddr + ":" + sPort);

            System.out.println("Loaded filters: #" + classi.size());

        }

        public void run(){

            while(Thread.currentThread()==waitTask){

                try{

                    Socket s=ss.accept();

                    Twins session=new Twins(s);

                }catch(Exception e){

                    e.printStackTrace();

                    return;

                }

            }

        }

        class Twins {

            Socket s;

            Socket servS;

            InputStreamReader cIs;

            OutputStreamWriter cOs;

            InputStreamReader sIs;

            OutputStreamWriter sOs;

            RetFlag cont=new RetFlag(OPEN);

            Listener[] filtri=new Listener[classi.size()];

            ConnectionTask client, server;

            boolean active=true;

            Twins(Socket s) throws Exception{

                this.s = s;

                s.setSoTimeout(0);

                cIs = new InputStreamReader(s.getInputStream());

                cOs = new OutputStreamWriter(s.getOutputStream());

                System.out.println("Connection with client accepted!");

                servS = new Socket(sAddr, sPort);

                servS.setSoTimeout(0);

                sIs = new InputStreamReader(servS.getInputStream());

                sOs = new OutputStreamWriter(servS.getOutputStream());

                System.out.println("Connection accepted by server!");

                int i=0;

                for(Enumeration e=classi.elements();e.hasMoreElements();){

                    Class c=(Class)e.nextElement();

                    filtri[i++] = (Listener)c.newInstance();

                }

                client=new ConnectionTask(CLIENT, cIs, sOs);

                server=new ConnectionTask(SERVER, sIs, cOs);

            }

            synchronized void cleanUp(){

                System.out.println("Connections closing...");

                if(!active) return;

                active=false;

                try{s.close();}catch(Exception x){}

                try{servS.close();}catch(Exception x){}

                if(filtri!=null){

                    for(int i=0;i<filtri.length;i++){

                        filtri[i].cleanUp();

                        filtri[i]=null;

                    }

                    filtri=null;

                }

                System.out.println("Connections closed!");

            }

            void update(int origin, Twins PetrosinoInstance, String message, char[] buffer, RetFlag cont){

                for(int i=0;i<filtri.length;i++){

                    if(cont.getValue()!=STOP)

                        filtri[i].step(origin, PetrosinoInstance, message.toUpperCase(), buffer, cont);

                }

            }

            class ConnectionTask extends Thread{

                int side;

                InputStreamReader is;

                OutputStreamWriter os;

                ConnectionTask(int side, InputStreamReader is, OutputStreamWriter os) throws Exception{

                    this.side=side;

                    this.is=is;

                    this.os=os;

                    setDaemon(true);

                    start();

                }

                public void run(){

                    char[] sb=new char[8192];

                    int ind = 0;

                    try{

                        while(active){

                            int ch = is.read();

                            if(!active) break;

                            if(ch>=0) sb[ind++]=(char)ch;

                            if(ch==10 || ch==13 || ind==8192){

                                if(ch>=32 || ind>1){

                                    String s = new String(sb,0,ind-((ind==8192)?0:1));

                                    if(srFlag){

                                        synchronized(System.out){

                                            if(side==SERVER) System.out.println("S>" + s);

                                            else System.out.println("C>" + s);

                                        }

                                    }

                                    synchronized(this$1){

                                        update(side, this$1, s, sb, cont);

                                    }

                                }

                                if(cont.getValue()==OPEN){

                                    os.write(sb, 0, ind);

                                    os.flush();

                                }else if(cont.getValue()==STOP){

                                    break;

                                }

                                ind=0;

                            }

                        }

                    }catch(Exception e){

                        if(side==CLIENT){

                            System.out.print("Client side: ");

                        }else{

                            System.out.print("Server side: ");

                        }

                        e.printStackTrace();

                    }finally{

                        cleanUp();

                    }

                }

            }

        }

        public interface Listener {

            public void cleanUp();

            public void step(int origin, Twins PetrosinoInstance, String message, char[] buffer, RetFlag cont);

        }

        class RetFlag{

            int command;

            public RetFlag(int init){command=init;}

            public void setValue(int cmd){command=cmd;}

            public int getValue(){return command;}

        }

    }

Qualche applicazione pratica

Tanto per inquadrare i possibili usi di un proxy generico, pensiamo ad un monitoraggio delle comunicazione di mail interne ad un'azienda. In partcolare vogliamo:

Per meglio capire il funzionamento di questo filtro bisognerebbe dare uno sguardo alle varie specifiche dei protocolli SMTP, MIME etc… Onde evitare simile onere al lettore includo nel seguito due listati di output (ottenuti proprio da Petrosino) che danno una idea sufficiente dell’interscambio che avviene tra client e server durante la spedizione di una mail (‘C>’ sta per client e ‘S>’ per server):
 
Mail senza allegati
(oggetto: "prova", contenuto: "prova")
Mail con un piccolo file grafico in allegato
(oggetto: "Prova", contenuto: "Prova", allegato: " BULLETB1.GIF")
C>HELO pce21
S>250 mailserver.ditta.com
C>RSET
S>250 OK
C>MAIL FROM:<spinelli.p@ditta.com>
S>250 OK
C>RCPT TO:<amico.mio>
S>250 OK
C>DATA
S>354 Enter Mail, end by a line with only '.'
C>X-Sender: spinelli.p@smtp.ditta.com (Unverified)
C>X-Mailer: Windows Eudora Light Version 1.5.2
C>Mime-Version: 1.0
C>Content-Type: text/plain; charset="us-ascii"
C>To: amico.mio
C>From: Spinelli Piero <spinelli.p@ditta.com>
C>Subject: prova
C>prova
C>.
S>250 Message received OK.
C>QUIT
S>221 GoodBye
C>HELO pce21
S>250 mailserver.ditta.com
C>RSET
S>250 OK
C>MAIL FROM:<spinelli.p@ditta.com>
S>250 OK
C>RCPT TO:<amico.mio>
S>250 OK
C>DATA
S>354 Enter Mail, end by a line with only '.'
C>X-Sender: spinelli.p@smtp.ditta.com (Unverified)
C>X-Mailer: Windows Eudora Light Version 1.5.2
C>Mime-Version: 1.0
C>Content-Type: multipart/mixed; boundary="=====================_906569772==_"
C>To: amico.mio
C>From: Spinelli Piero <spinelli.p@ditta.com>
C>Subject: prova
C>X-Attachments: C:\TRANSITO\EUDORA\BULLETB1.GIF;
C>--=====================_906569772==_
C>Content-Type: text/plain; charset="us-ascii"
C>prova
C>--=====================_906569772==_
C>Content-Type: image/gif; name="BULLETB1.GIF";
C> x-mac-type="47494666"; x-mac-creator="4A565752"
C>Content-Transfer-Encoding: base64
C>Content-Disposition: attachment; filename="BULLETB1.GIF"
C>R0lGODlhGQANALP/AMDAwGbM/2bMzDPM/zPMzDOZzDNmzDMzzDMzmQBmzAAzzAAzmQAAmQAAAAAA
C>AAAAACH5BAEAAAAALAAAAAAZAA0AQAQ/0AhAq70YC1KKWUomVkfnGcl4eWprGQTnzp2BggzjEgI7
C>Y4XB5DeScHzElymBOCySAJMNoXj+AkdDKNkrQC0RADs=
C>--=====================_906569772==_--
C>.
S>250 Message received OK.
C>QUIT
S>221 GoodBye

Ed ecco il listato di MailExtFilter, che registra su file il log delle mail spedite ed impedisce di spedire allegati all’esterno della ditta:
 
import java.io.*;

import java.util.*;

import Petrosino;

import Petrosino.*;

import java.text.SimpleDateFormat;

public class MailExtFilter implements Petrosino.Listener {

    private static final SimpleDateFormat df = new SimpleDateFormat("dd-MMMM-yyyy hh.mm.ss", Locale.ITALY);

    private static int instances=0;

    private static Object cond=new Object();

    private static final int ST_IDLE=0;

    private static final int ST_ADDR=1;

    private static final int ST_HEAD=2;

    private static final int ST_SINGLE=3;

    private static final int ST_MULTI=4;

    private static final int ST_DATA=5;

    private static final int ST_FINAL=6;

    private static final String DOMINIO_INTERNO="ditta.com";

    private static final String LOG_FILE="c:\\Temp\\MailExtFilter.log";

    private static FileWriter log=null;

    private String logMess=df.format(new Date());

    private boolean isExt = false;

    private boolean hasAttach = false;

    private int status=ST_IDLE;

    private String boundary=null;

    MailExtFilter(){

        super();

        try{

            synchronized(cond){

                instances++;

                if(log==null) log=new FileWriter(LOG_FILE, true);

            }

        }catch(Exception e){

            e.printStackTrace();

        }

    }

    private void log(String s){

        try{

            synchronized(log){

                log.write(s,0,s.length());

                log.flush();

            }

        }catch(Exception e){}

    }

    public void terminate(){cleanUp();}

    public void cleanUp(){

        if(logMess!=null && logMess.length()>0) log(logMess+"\n");

        logMess=null;

        try{

            synchronized(cond){

                instances--;

                if(instances==0) log.close();

                log=null;

            }

        }catch(Exception e){}

    }

    public void step(int origin, Twins PetrosinoInstance, String message, char[] buffer, RetFlag cont){

        if(origin==Petrosino.CLIENT){

            if(status==ST_IDLE){

                if(message.startsWith("HELO ")){

                    logMess+=(" - "+message);

                    status=ST_ADDR;

                }

            }else if(status==ST_ADDR){

                if(message.startsWith("MAIL FROM:")){

                    logMess+=(" - "+message);

                }else if(message.startsWith("RCPT TO:")){

                    logMess+=(" - " + message);

                    int at=message.indexOf("@");

                    if(at>=0 && message.indexOf(DOMINIO_INTERNO)!=(at+1)) isExt=true;

                }else if(message.equals("DATA")){

                    status=ST_HEAD;

                }

            }else if(status==ST_HEAD){

                if(message.startsWith("CONTENT-TYPE: MULTIPART/MIXED; BOUNDARY=\"")){

                    hasAttach=true;

                    logMess+=(" - ATTACHMENTS");

                    if(isExt){

                        logMess+=(" (Not Allowed)");

                        cont.setValue(Petrosino.CLOSE);

                    }

                    logMess+=":";

                    boundary=message.substring(41);

                    int ind=(boundary.indexOf("\""));

                    if(ind<1){

                        logMess+=" *** ERROR IN MESSAGE STRUCTURE ***";

                        cont.setValue(Petrosino.STOP);

                        status=ST_IDLE;

                    }else{

                        boundary=boundary.substring(0,ind);

                        status=ST_MULTI;

                    }

                }else if(message.startsWith("CONTENT-TYPE:")){

                    logMess+=(" - " + message);

                    status=ST_SINGLE;

                }

            }else if(status==ST_SINGLE){

                if(message.equals(".")){

                    status=ST_FINAL;

                }

            }else if(status==ST_MULTI){

                if(message.startsWith("CONTENT-DISPOSITION: ATTACHMENT;")){

                    logMess+=(" - " + message.substring(33));

                    status=ST_DATA;

                }

            }else if(status==ST_DATA){

                if(message.startsWith("--"+boundary+"--")){

                    status=ST_SINGLE;

                }else if(message.startsWith("--"+boundary)){

                    status=ST_MULTI;

                }

            }else if(status==ST_FINAL){

                if(message.equals("QUIT")){

                    log(logMess+" OK!\n");

                    logMess="";

                    if(cont.getValue()==Petrosino.CLOSE) cont.setValue(Petrosino.STOP);

                }else if(message.equals("RSET")){

                    log(logMess+" OK!\n");

                    int ind=logMess.indexOf(" - ");

                    ind=logMess.indexOf(" - ", ind+1);

                    logMess=logMess.substring(0, ind);

                    if(cont.getValue()==Petrosino.CLOSE) cont.setValue(Petrosino.OPEN);

                    status=ST_ADDR;

                }else{

                    logMess+=SMU(message);

                }

            }

        }else{

            if(status==ST_IDLE){

                if(!(message.startsWith("250 ") || message.startsWith("220 "))){

                    logMess+=SMU(message);

                    cont.setValue(Petrosino.STOP);

                }

            }else if(status==ST_ADDR){

                if(!message.startsWith("250 ")){

                    logMess+=SMU(message);

                    cont.setValue(Petrosino.STOP);

                }

            }else if(status==ST_HEAD){

                if(!message.startsWith("354 ")){

                    logMess+=SMU(message);

                    cont.setValue(Petrosino.STOP);

                }

            }else if(status==ST_SINGLE){

                logMess+=SMU(message);

                cont.setValue(Petrosino.STOP);

            }else if(status==ST_MULTI){

                logMess+=SMU(message);

                cont.setValue(Petrosino.STOP);

            }else if(status==ST_DATA){

                logMess+=SMU(message);

                cont.setValue(Petrosino.STOP);

            }else if(status==ST_FINAL){

                if(message.startsWith("221 ")){

                    status=ST_IDLE;

                }else if(!message.startsWith("250 ")){

                    logMess+=SMU(message);

                    cont.setValue(Petrosino.STOP);

                }

            }

        }

    }
    private String SMU(String m){
        return " *** SERVER MESSAGE UNASPECTED: '" + m +"' ***";
    }
}

Per attivare il filtro bisigna digitare, ad esempio:

>java Petrosino 25 mailserver 25 0 MailExtFilter
Ammettiamo adesso di aver rilevato una nuova esigenza, ovvero quella di monitorare il carico di rete dovuto alla mail. E’ possibile scrivere un nuovo filtro, CountExtFilter, per registrare su file (o su db, via JDBC) i byte scambiati ad ogni invio. I due filtri sono attivabili a cascata:
>java Petrosino 25 mailserver 25 0 MailExtFilter CountExtFilter
In questo modo è possibile aggiungere al proxy generico un diverso filtro per ogni nuova esigenza di monitoraggio o filtratura (ovviamente riguardanti lo stesso servizio).

Riassumendo

Scrivere programmi di comunicazione, come questo mini proxy, è più semplice in Java che in altri linguaggi grazie alle classi di comunicazione, ai thread, al link dinamico, all’innestamento delle classi e, da non dimenticare, alla possibilità di far migrare senza modifiche il modulo che girava su UNIX sul nuovo server NT (o meglio, da NT al nuovo server UNIX, ….). Come esercizio, chi fosse interessato, potrebbe provare a scrivere un filtro per Web-Server che tenga traccia di tutte le pagine scaricate. La specifica HTTP è lunga e tediosa quindi suggerisco di far girare Petrosino, senza alcun filtro sul vostro client:

>java Petrosino 80 WebServer 80 1
(dove WebServer è l’indirizzo di un qualsiasi server web), e richiedere pagine dal browser usando la URL iniziale:

    http://localhost/

Credo che l’output del programma sia, ai fini didattici, un ottimo inizio per capire i protocolli internet e, perché no, per pasticciarci un po’.

Risorse

Piergiuseppe Spinelli svolge attività di analista/programmatore dal 1980. Si è occupato di training e di sviluppo di sistemi, particolarmente nel campo della supervisione di processo. Ha lavorato per aziende dei gruppi Saint Gobaing, Angelini, Procter&Gamble, Alcatel/Telettra, SIV e per vari enti pubblici e privati. E' contattabile all'indirizzo spinellip@sgol.it o al sito www.GeoCities.com/Eureka/Enterprises/9607.
 
 
 
 

MokaByte Web  1998 - www.mokabyte.it 
MokaByte ricerca nuovi collaboratori. 
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it