In questa lezione completiamo il programma Java che simula una comunicazione Client/Server basata sul protocollo DTLS. Iniziamo con metodo handshake()
fornendo le implementazioni dei metodi produceHandshakePackets()
e runDelegatedTasks()
:
private List<DatagramPacket> produceHandshakePackets(
SSLEngine engine, SocketAddress address) throws Exception {
List<DatagramPacket> packets = new ArrayList<>();
boolean endLoops = false;
int loops = 60;
while (!endLoops) {
if (--loops < 0) {
throw new RuntimeException(
"Too much loops to produce handshake packets");
}
ByteBuffer oNet = ByteBuffer.allocate(32768);
ByteBuffer oApp = ByteBuffer.allocate(0);
SSLEngineResult r = engine.wrap(oApp, oNet);
oNet.flip();
SSLEngineResult.Status rs = r.getStatus();
SSLEngineResult.HandshakeStatus hs = r.getHandshakeStatus();
if (null != rs)
switch (rs) {
case BUFFER_OVERFLOW:
throw new Exception("Buffer overflow: " +
"incorrect server maximum fragment size");
case BUFFER_UNDERFLOW:
if (hs != NOT_HANDSHAKING) {
throw new Exception("Buffer underflow: " +
"incorrect server maximum fragment size");
}
break;
case CLOSED:
throw new Exception("SSLEngine has closed");
default:
break;
}
if (oNet.hasRemaining()) {
byte[] ba = new byte[oNet.remaining()];
oNet.get(ba);
DatagramPacket packet = new DatagramPacket(ba, ba.length, address);
packets.add(packet);
}
boolean endInnerLoop = false;
SSLEngineResult.HandshakeStatus nhs = hs;
while (!endInnerLoop) {
if (nhs == NEED_TASK) {
runDelegatedTasks(engine);
nhs = engine.getHandshakeStatus();
} else if ((nhs == FINISHED) ||
(nhs == NEED_UNWRAP) ||
(nhs == NOT_HANDSHAKING)) {
endInnerLoop = true;
endLoops = true;
} else if (nhs == NEED_WRAP) {
endInnerLoop = true;
}
}
}
return packets;
}
private void runDelegatedTasks(SSLEngine engine) throws Exception {
Runnable runnable;
while ((runnable = engine.getDelegatedTask()) != null) {
runnable.run();
}
SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus();
if (hs == SSLEngineResult.HandshakeStatus.NEED_TASK) {
throw new Exception("handshake shouldn't need additional tasks");
}
}
Mentre con il metodo runDelegatedTask()
ci assicuriamo che i task interni SSL vengono eseguiti rispettando la macchina a stati finiti dell'SSLEngine, il metodo produceHandshakePackets()
, attraverso un ciclo con un limite massimo al numero di iterazioni, si preoccupa di consumare i dati dalla rete processando successivamente il risultato ottenuto, e di produrre, se necessario, pacchetti di risposta da inviare sulla rete.
Proseguiamo aggiungendo le variabili a livello di classe, che definiscono le socket e gli indirizzi necessari per la comunicazione Client/Server, ed il metodo main
che permette l'esecuzione dell'applicativo:
volatile private DatagramSocket clientSocket;
volatile private DatagramSocket serverSocket;
volatile private InetSocketAddress serverAddress;
volatile private InetSocketAddress clientAddress;
public static void main(String[] args) throws Exception{
DtlsDatagramDemo client = new DtlsDatagramDemo();
DtlsDatagramDemo server = new DtlsDatagramDemo();
client.clientSocket = new DatagramSocket();
server.serverSocket = new DatagramSocket();
client.serverAddress = new InetSocketAddress(InetAddress.getLocalHost(),
server.serverSocket.getLocalPort());
server.clientAddress = new InetSocketAddress(
InetAddress.getLocalHost(), client.clientSocket.getLocalPort());
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> list = new ArrayList<>();
try {
list.add(pool.submit(() -> {
try {
client.client();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (client.clientSocket != null) {
client.clientSocket.close();
}
}
return "Complimenti, client!";
}));
list.add(pool.submit(() -> {
try {
server.server();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (server.serverSocket != null) {
server.serverSocket.close();
}
}
return "Complimenti, server!";
}));
} finally {
pool.shutdown();
}
list.forEach((fut) -> {
try {
System.out.println(fut.get());
} catch (CancellationException |
InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
Il codice presente all'interno del metodo main
non fa altro che eseguire il lato client e quello server su Thread separati avvalendosi di un ExecutorService. I metodi client()
e server()
della classe DtlsDatagramDemo
, incaspulano le fasi di comunicazione delle due entità coinvolte:
private void client() throws Exception{
clientSocket.setSoTimeout(10000);
// create SSLEngine
SSLEngine engine = getClientSSLEngine();
// handshaking
handshake(engine, clientSocket, serverAddress,"Client");
deliverAppData(engine, clientSocket, ByteBuffer.wrap("Ciao Server, Sono il Client".getBytes()), serverAddress);
receiveAppData(engine, clientSocket, ByteBuffer.wrap("Ciao Client, Sono il Server".getBytes()));
}
private void server() throws Exception {
serverSocket.setSoTimeout(10000);
// create SSLEngine
SSLEngine engine = getServerSSLEngine();
// handshaking
handshake(engine, serverSocket, clientAddress,"Server");
// read client application data
receiveAppData(engine, serverSocket, ByteBuffer.wrap("Ciao Server, Sono il Client".getBytes()));
// write server application data
deliverAppData(engine, serverSocket, ByteBuffer.wrap("Ciao Client, Sono il Server".getBytes()), clientAddress);
}
Entrambe le implementazioni seguono esattamente le stesse fasi, ottengono un SSLEngine, avviano la procedura di handshake e, se la procedura si conclude con successo, proseguono con lo scambio dei dati. Anche i metodi receiveAppData()
e deliverAppData()
risultano essere di facile comprensione, svolgono essenzialmente la ricezione e la trasmissione di byte attraverso le socket ricevute in ingresso:
//deliver application data
private void deliverAppData(SSLEngine engine, DatagramSocket socket,
ByteBuffer appData, SocketAddress peerAddr) throws Exception {
// Note: have not consider the packet loses
List<DatagramPacket> packets =
produceApplicationPackets(engine, appData, peerAddr);
appData.flip();
for (DatagramPacket p : packets) {
socket.send(p);
}
}
//receive application data
private void receiveAppData(SSLEngine engine,
DatagramSocket socket, ByteBuffer expectedApp) throws Exception {
for(int loops=60;;--loops) {
if (loops < 0) {
throw new RuntimeException(
"Too much loops to receive application data");
}
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
ByteBuffer netBuffer = ByteBuffer.wrap(buf, 0, packet.getLength());
ByteBuffer recBuffer = ByteBuffer.allocate(1024);
SSLEngineResult rs = engine.unwrap(netBuffer, recBuffer);
recBuffer.flip();
if (recBuffer.remaining() != 0) {
printData("Received application data", recBuffer);
if (!recBuffer.equals(expectedApp)) {
System.out.println("Engine status is " + rs);
throw new Exception("Not the right application data");
}
break;
}
}
}
private void printData(String prefix, ByteBuffer bb) {
System.out.println(prefix+" "+new String(bb.array()));
}
Abbiamo terminato la codifica del programma che, riepilogando, simula la comunicazione Client/Server con protocollo DTLS attraverso due thread sparati. Si è scelto, utilizzando l'SSLEngine, di fare in modo che la comunicazione richieda la mutua autenticazione attraverso lo scambio dei certificati. Se eseguiamo il programma dovremmo ottenere un risultato del tipo:
Received application data Ciao Server, Sono il Client
Received application data Ciao Client, Sono il Server
Complimenti, client!
Complimenti, server!
Si tratta dello scenario in cui la procedura di handshake si è conclusa con successo, è interessante far notare come agendo sul codice di creazione del contesto SSLEngine del client o del server sia possibile far fallire l'handshake.
Ad esempio possiamo fare in modo che il client non fornisca il certificato o che ne fornisca uno non presente nel truststore del server nei casi in cui l'autenticazione del client sia richiesta (come accade nel codice implementato).