Il tunneling HTTP

Comunicare con un‘applicazione serverdi

Il tunneling HTTP e le applicazioni J2EE. Comunicare, in modo relativamente semplice, con un‘applicazione server tramite il protocollo HTTP

Introduzione

Le applicazioni client/server possono usare vari protocolli di comunicazione ma considerando il World Wide Web si tratta quasi sempre di applicazioni basate sul protocollo HTTP.
Pur essendo meno efficiente di una connessione socket e meno elegante di una chiamata RMI, HTTP ha il grosso vantaggio di attraversare facilmente i firewall e i proxy aziendali; inoltre le API Java e l'architettura J2EE consentono di sviluppare agevolmente delle complete applicazioni client/server.
Con il tunneling HTTP infatti si sfrutta l'infrastruttura messa a disposizione dal protocollo HTTP per far comunicare due applicazioni sulla rete, inserendo i dati necessari al loro colloquio all'interno della richiesta e della risposta HTTP.
Utilizzare HTTP come mezzo di trasporto dei dati tra le applicazioni non è ovviamente una novità, XML-RPC e i Web Services sono esempi di tecnologie standard che usano il tunneling HTTP e XML per far comunicare due applicazioni eterogenee.

Il tunneling HTTP

HTTP [1] è il protocollo di comunicazione più usato sul web e come tale è riconosciuto dalla maggior parte dei firewall e dei proxy aziendali. Le sue caratteristiche aperte, basate su un meccanismo di richiesta/risposta, lo rendono adatto ad essere usato come protocollo di comunicazione per un gran numero di applicazioni client/server.
Di conseguenza i dati applicativi, formattati come flusso di bytes, documento XML o elenco di properties, possono essere inseriti facilmente all'interno del corpo del messaggio HTTP e inviati o ricevuti come normali richieste o risposte HTTP.
L'applicazione client, simulando il funzionamento di un browser web, comunica con il server generando una richiesta HTTP, dove l'URL individua l'applicazione server, mentre le variabili CGI o i dati applicativi inseriti nel corpo del messaggio HTTP, rappresentano i parametri di input.
L'applicazione server, attivata dalla richiesta del client, recupera i parametri di input e produce come output una risposta HTTP con i dati applicativi cablati nel corpo del messaggio che vengono infine recuperati dall'applicazione client.
Dal punto di vista dell'architettura J2EE il client può essere una applicazione Java standalone, una applet, una servlet e al limite anche un EJB; il server invece viene spesso implementato come una servlet.

Figura 1. Il tunneling HTTP.

Gestione di una richiesta HTTP

Cominciamo ad esaminare il codice necessario a generare una richiesta HTTP da parte di una applicazione client. Nelle istruzioni seguenti vediamo come è possibile trasmettere un elenco di variabili CGI o un oggetto Java serializzabile come parametri di input e parallelamente esaminiamo anche il codice lato server:


//crea l'URL e la connessione HTTP con la servlet
URL url = new URL("http://localhost:8080/test/tunnelingHTTP/DataServlet");
HttpURLConnection httpURLConnection
= (HttpURLConnection)url.openConnection();
//abilita la connessione per l'input, l'output e disabilita la cache
httpURLConnection.setDoInput(true);
httpURLConnection.setDoOutput(true);
httpURLConnection.setUseCaches(false);

la classe Base64.java ? il sorgente è incluso negli esempi dell'articolo ? codifica un flusso di bytes in codice base64 secondo le specifiche descritte dalla RFC-1521 [2]; in questo caso però viene usata per convertire in base64 la stringa contenente l'utente e la password di accesso al sito così come richiesto dalla RFC-2617 [1] per la Basic Authentication:


//imposta l'utente e la password di accesso con basic authentication
String userPassword = user + ":" + password;
httpURLConnection.setRequestProperty("Authorization",
    "Basic " + new String(Base64.encode(userPassword.getBytes())));

nel caso di parametri CGI:


//imposta il valore delle variabili CGI
String parameter1 = . . .;
String parameter2 = . . .;
. . .

//crea l'elenco delle variabili CGI
HashMap cgi = new HashMap();
cgi.put("parameter1", parameter1);
cgi.put("parameter2", parameter2);

il metodo setRequestProperty imposta il contenuto MIME del messaggio e application/x-www-form-urlencoded indica un elenco di variabili CGI trasmesse con metodo POST:


//crea una richiesta HTTP con metodo POST e include
//le variabili CGI nel corpo del messaggio
httpURLConnection.setRequestProperty("Content-Type",
    "application/x-www-form-urlencoded");            
DataOutputStream dataOutputStream
    = new DataOutputStream(httpURLConnection.getOutputStream());
dataOutputStream.writeBytes(cgiToString(cgi));
dataOutputStream.close();
. . .

il metodo cgiToString converte le variabili CGI in una stringa URL-encoded secondo le specifiche riportate nella RFC ?2616 [1]:


/**
* Converte i parametri CGI in una stringa codificata da usare nella richiesta HTTP.
*
* @param cgi e' l'elenco delle variabili CGI da codificare
* @return la stringa con le variabili CGI codificate
*/
private String cgiToString(HashMap cgi)
{
    try
    {
        StringBuffer stringBuffer= new StringBuffer();
        Iterator iterator = cgi.keySet().iterator();
        while (iterator.hasNext())
        {
            String name = (String) iterator.next();
            String value = (String) cgi.get(name);
            stringBuffer.append(URLEncoder.encode(name, "8859_1"))
.append('=')
.append(URLEncoder.encode(value, "8859_1"));
            if (iterator.hasNext()) stringBuffer.append('&');
        }
        return stringBuffer.toString();
    }
    catch(UnsupportedEncodingException e)
    {
        return null;
    }
}

invece nel caso di oggetto serializzato il codice diventa:


//crea un oggetto con i parametri di input
DataInput input = new DataInput();
. . .

il contenuto MIME application/x-java-serialized-object indica un oggetto Java serializzato:


//invia i parametri di input alla servlet
//come oggetto serializzato
httpURLConnection.setRequestProperty("Content-Type",
"application/x-java-serialized-object");
ObjectOutputStream outputStream
= new ObjectOutputStream(httpURLConnection.getOutputStream());
outputStream.writeObject(input);
outputStream.close();
. . .

adesso esaminiamo il codice lato server. Nel nostro caso si tratta si una servlet quindi è molto semplice recuperare i parametri CGI:


String parameter1 = httpServletRequest.getParameter("parameter1");
String parameter2 = httpServletRequest.getParameter("parameter2");
. . .

invece nel caso di oggetto serializzato il codice diventa:



//riceve i parametri di input inviati dal client
DataInput input;
. . .

ObjectInputStream inputStream
= new ObjectInputStream(httpServletRequest.getInputStream());
try
{
    input = (DataInput)inputStream.readObject();
    . . .
}
catch(ClassNotFoundException e)
{
    . . .
}
inputStream.close();
. . .

Gestione di una risposta HTTP

Riprendendo l'esempio del paragrafo precedente vediamo ora come la servlet genera una risposta HTTP a partire sempre da un oggetto serializzabile:



//crea un oggetto con i parametri di output
DataOutput output = new DataOutput();
. . .

il metodo setContentType imposta il contenuto MIME del messaggio e application/x-java-serialized-object indica un oggetto Java serializzato


//invia i parametri di output all'applicazione client
httpServletResponse.setContentType("application/x-java-serialized-object");
ObjectOutputStream outputStream
= new ObjectOutputStream(httpServletResponse.getOutputStream());
outputStream.writeObject(output);
outputStream.close();
. . .

parallelamente l'applicazione client riceve i parametri di output con il seguente codice:


//riceve i parametri di output della servlet come oggetto serializzato
DataOutput output;
. . .

ObjectInputStream inputStream
= new ObjectInputStream(httpURLConnection.getInputStream());
try
{
    DataOutput output = (DataOutput) inputStream.readObject();
    . . .
}
catch(ClassNotFoundException e)
{
    . . .
}
inputStream.close();
. . .

L'ultima considerazione riguarda l'uso di DataInputStream e DataOutputStream al posto di ObjectInputStream e ObjectOutputStream per inviare (o ricevere) flussi di bytes, stringhe che rappresentano documenti XML o elenchi di proprietà anche a server (da client) non-Java; un esempio di implementazione si trova nelle classi StringApplet e StringServlet incluse negli esempi dell'articolo.

Infine vediamo come si presentano la richiesta e la risposta HTTP generate dal codice che abbiamo appena esaminato nel caso di oggetti Java serializzati. Chi ha una certa pratica di HTTP noterà sicuramente la parentela che lega le intestazioni di questi messaggi a quelle generate da un borwser e da un server web:

Figura 2. Header e body della request HTTP.

Figura 3. Header e body della response HTTP.

Introduzione a SSL

Il tunneling HTTP funziona anche con il protocollo sicuro HTTPS, ossia HTTP su una connessione SSL [3].
SSL (Secure Sockets Layer) è il protocollo di sicurezza più diffuso sul web; infatti viene usato per fornire la sicurezza nelle comunicazioni client/server tramite l'autenticazione delle parti coinvolte e la crittografia dei dati scambiati durante il collegamento. Ecco una breve descrizione:

autenticazione

garantisce l'identità delle parti coinvolte nella comunicazione tramite l'uso delle chiavi assimetriche, ovvero delle chiavi private e delle corrispondenti chiavi pubbliche usate sotto forma di certificati personali.
Lo scopo di un certificato personale è quello di informare la controparte sull'identità del proprietario e per assolvere a questo compito contiene, oltre alla sua chiave pubblica, anche i suoi dati identificativi e una firma digitale che ne attesta la validità.
Nella maggior parte dei casi i certificati personali usati da SSL vengono emessi dalle CA (Certification Authority), le quali garantiscono l'identità del proprietario firmando, con la propria chiave privata, la sua chiave pubblica.
Un certificato può essere prodotto anche dallo stesso proprietario della chiave pubblica che si assume ufficiosamente il ruolo di CA e lo firma usando la corrispondente chiave privata; in questo caso si parla di certificati autofirmati.
SSL supporta sia l'autenticazione lato server che quella lato client; nel primo caso, il più comune, controlla solo l'identità del server, mentre nel secondo caso controlla anche l'identità del client.
Il client controlla l'identità del server verificando che il certificato personale inviato dal server sia firmato da una CA che riconosce; per fare ciò deve disporre di un certificato autofirmato o di un certificato radice emesso dalla CA che ha firmato la chiave pubblica del server.
Nel caso di autenticazione lato client invece, è il server che controlla l'identità del client verificando che il certificato inviato da quest'ultimo sia firmato da una CA riconosciuta.
Pertanto nell'autenticazione lato server, il client deve possedere solo il certificato autofirmato o il certificato radice della CA del server, mentre nell'autenticazione lato client deve possedere, oltre al certificato autofirmato o al certificato radice della CA del server, anche il suo certificato personale da inviare al server.

crittografia

garantisce la riservatezza e l'integrità della comunicazione mediante la cifratura dei dati che vengono scambiati durante il collegamento. In questo caso, per motivi di efficenza, i dati vengono cifrati usando una chiave simmetrica temporanea che viene generata dal client dopo la fase di autenticazione, e inviata al server dopo essere stata crittografata con la chiave pubblica di quest'ultimo.

Le chiavi private e i cerificati sono contenuti in particolari file chiamti keystore e truststore; in particolare il keystore contiene le chiavi private con i corrispondenti certificati personali, mentre il truststore contiene i certificati autofirmati o i certificati radice delle CA.
Keystore e truststore sono gestiti tramite l'utility keytool [5] del JDK.

JSSE (Java Secure Socket Extension) [4] è l'implentazione Java di SSL e di HTTPS; il pacchetto java.security invece fornisce le API per la gestione delle chiavi e dei certificati.
Per concludere, un grosso vantaggio di JSSE è dovuto al fatto che, una volta definite le chiavi e il protocollo da usare, tutti i passaggi necessari a garantire la sicurezza della comunicazione sono automatici e del tutto trasparenti all'applicazione che lo usa.

Purtroppo una descrizione completa di SSL e JSSE esula dagli scopi di questo articolo, quindi per ulteriori approfondimenti si rimanda alla bibliografia in fondo alla pagina.

Generazione di una richiesta HTTPS

Finalmente possiamo vedere come usare JSSE per generare una richiesta HTTPS. Nel caso specifico esaminiamo il codice necessario ad aprire una connessione HTTPS con un sever che richiede l'autenticazione lato client, quindi abbiamo bisogno anche di un keystore:


//carica dal keystore la chiave privata
//e il certificato personale del client
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(new FileInputStream("d:/tunnelingHTTP/clientKeyStore.jks"),
"keyPassword".toCharArray());
KeyManagerFactory keyManagerFactory
= KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, "myKeyPassword".toCharArray());

se il sito è protetto con autenticazione lato server non c'è bisogno di avere un keystore e il codice precedente è può essere ignorato


//carica dal truststore il certificato autofirmato
//o radice della CA del server
KeyStore trustStore = KeyStore.getInstance("JKS");
trustStore.load(new FileInputStream("d:/tunnelingHTTP/clientTrustStore.jks"),
"trustPassword".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore );

l'autenticazione lato client richiede l'uso della versione 3 di SSL:


//crea il contesto SSL, usa come protocollo SSL versione 3
sslContext = SSLContext.getInstance("SSLv3");

//crea il contesto SSL per l'autenticazione lato client
sslContext.init(keyManagerFactory.getKeyManagers(),
trustManagerFactory.getTrustManagers(), null);

se il sito è protetto solo con l'autenticazione lato server basta avere il truststore e l'ultima istruzione diventa:


//crea il contesto SSL per l'autenticazione lato server
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);        

la creazione di un contesto SSL è un operazione abbastanza onerosa per la JVM, pertanto potrebbe essere conveniente implementare tutto il codice precedente come singleton [6].


//crea l'URL all'applicazione di download
URL urlSite = new URL("https://localhost:9043/cgi-bin/download.exe");

//se la fase di autenticazione ha avuto successo crea la connessione HTTPS
HttpsURLConnection httpsURLConnection
= (HttpsURLConnection) urlSite.openConnection();

//imposta la connessione HTTPS con il contesto SSL appena creato
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
. . .

da questo momento in poi tutti i dati che transitano sulla connessione vengono crittografati automaticamente da SSL

Java plug-in

L'esecuzione di codice Java all'interno di un browser web può creare parecchi problemi di compatibilità soprattutto se si usano le API avanzate.
Infatti le JMV integrate nella maggior parte dei browser supportano al massimo JDK 1.1 ma costringono difatto lo sviluppatore a limitarsi, per motivi di sicurezza, alla versione 1.0 con evidenti problemi nella scrittura del codice.
Per questo motivo Sun ha introdotto una tecnologia chiamata Java plug-in [6] che risolve il problema di compatibilità dei browser integrando un ambiente di run-time esterno, il JRE, indipendente dalla JVM del browser.
L'attivazione del plug-in avviene in maniera del tutto trasparente al browser attraverso la sostituzione all'interno della pagina HTML del tag applet con i tag object e embed; l'utility htmlconvert [7], inclusa nel JDK, automatizza questo processo di conversione.
Una interessante possibilità offerta dal plug-in Java è quella di poter scaricare il JRE direttamente dal server web dell'applicazione.
Comunque, nonostante le sue potenzialità, si preferisce limitare l'uso del plug-in alle applicazioni intranet, soprattutto per evitare all'utente internet il disagio di dover scaricare e installare, anche solamente la prima volta, l'ambiente di run-time Java.

Un esempio di comunicazione applet-servlet: applet2servlet

I primi due esempi inclusi nell'articolo riguardano un semplice colloquio tra una applet e una servlet protetta da basic authentication.
Il primo esempio mostra come l'applet DataApplet comunica con la servlet DataServlet usando come input e output due oggetti serializzati, DataInput e DataOutput.
Il secondo esempio dimostra come l'applet StringApplet invia e riceve una stringa (o flusso di bytes) dalla servlet StringServlet.
Lo script buildHtml.bat invece è un esempio di utilizzo dello strumento htmlconvert.

Un esempio di client HTTPS: servlet2cgi

L'ultimo esempio invece mostra come un client HTTPS, la servlet DownloadServlet, possa visualizzare un file multimediale all'interno del browser scaricandolo da un sito protetto con l'autenticazione lato client.
Inoltre la servlet mostra come inviare i parametri di input all'applicazione di download usando le variabili CGI.
Per quanto riguarda la gestione delle chiavi, lo script buildKeys.bat mostra come usare keytool per creare il keystore e il truststore con i certificati autofirmati di cui abbiamo bisogno.

Conclusioni

Nonostante il tunneling HTTP consenta di sviluppare agevolmente applicazioni client/server affidabili e sicure, nell'utilizzo reale vanno considerati con attenzione eventuali problemi derivanti dall'abuso di HTTP, soprattutto quando l'efficenza è importante e il client genera numerose richieste in brevi intervalli di tempo.
Peraltro in questi casi, soprattutto in ambito intranet, la soluzione migliore potrebbe riguardare l'uso di altre tecnologie quali ad esempio i raw socket o RMI-IIOP.

Bibliografia

[1]
http
http://www.w3.org/Protocols/

[2]
base64
http://www.ietf.org/rfc/rfc1521.txt

[3]
SSL
http://java.sun.com/j2se/1.4.2/docs/guide/security/jsse/JSSERefGuide.html

[4]
JSSE
http://java.sun.com/j2se/1.4.2/docs/guide/security/jsse/JSSERefGuide.html

[5]
keytool
http://java.sun.com/j2se/1.4.2/docs/tooldocs/tools.html

[6]
Singleton
http://www.javaworld.com/columns/jw-java-design-patterns-index.shtml

[7]
Java plug-in
http://java.sun.com/j2se/1.4.2/docs/guide/plugin/index.html

Risorse

È possibile scaricare gli esempi dal menu in alto a sinistra.

Condividi

Pubblicato nel numero
100 ottobre 2005
Simone Perina, si occupa di Java dal 1999; attualmente lavora per un importante Gruppo Bancario dove segue lo sviluppo di applicazioni informatiche a supporto dei canali innovativi, in particolare modo dell‘internet banking.
Ti potrebbe interessare anche