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
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
Figura 3
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
Scarica gli esempi applet2servlet
e servlet2cgi
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.
|