Revista Tecnología

Serialización de objetos utilizando la Streaming Library y desarrollo de plugins I2P

Publicado el 19 mayo 2015 por Debadastra @jdaanial

En la entrada anterior se ha venido hablado de los beneficios que aporta la Streaming Library si eres un usuario de I2P y quieres automatizar el proceso de crear “peers” descentralizados que, utilizando la red de I2P, puedan comunicarse y transferir información. Esta librería se encuentra escrita en Java y cuenta con todas las características necesarias para poder interactuar con el protocolo I2CP y poder desplegar servicios que utilicen/establezcan puentes de entrada y salida desde la instancia local de I2P. Tal como mencionaba en el artículo anterior, el procedimiento para crear un emisor y un receptor es bastante simple y si el lector alguna vez ha tenido que trabajar con sockets y programación en entornos de red en cualquier lenguaje, rápidamente podrá apreciar que aunque la librería aporta clases nuevas, su funcionamiento es muy similar a los clásicos sockets “servidor” y “cliente”, de hecho, las clases principales de la Streaming library son “wrappers” de las las clases “ServerSocket” y “Socket” de Java, aunque evidentemente implementando toda la lógica necesaria para poder interactuar con I2P.
No obstante, cuando se navega por Internet en busca de información sobre el uso avanzado de está librería normalmente es muy complicado encontrar detalles técnicos que permitan adentrarse en su uso y casi en todos los casos, se implementa el típico ejemplo del servidor “echo”, en el que existe un cliente que actúa como emisor, un servidor que actúa como receptor y el receptor devuelve los mensajes de texto que envía el emisor, funcionando como un servidor “echo” muy simple, no obstante, uno de los principales beneficios con los que cuenta Java, es que tiene un sistema muy potente de serialización de objetos que permite compartir información entre diferentes JVM, algo que potencio enormemente la evolución de tecnologías tales como RMI, CORBA, JINI y posteriormente los tan conocidos EJB en el mundo de J2EE. En este caso, también es posible serializar objetos entre emisores y receptores I2P, lo que supone un abanico de posibilidades enorme a la hora de crear aplicaciones que utilicen la capa de anonimato que aporta I2P a sus usuarios, ya que se pueden crear objetos debidamente preparados para transmitir mucha más información que una cadena de texto. Para los que no lo sepáis, para poder serializar un objeto en Java, dicho objeto, sus atributos y todas las clases padre de las que hereda deben ser serializables también, lo cual se consigue implementando la interfaz “java.io.Serializable”, además, si alguno de los atributos de dicha clase no es serializable, debe utilizarse el modificador de acceso “transient” con el fin de indicarle a la JVM que dicho atributo debe utilizarse únicamente en el contexto de la JVM local y no se debe incluir en el proceso de serialización del objeto.
Dicho esto, queda claro que una de las labores más importantes a la hora de diseñar una aplicación que aproveche los beneficios de la Streaming Library, consiste precisamente en definir una estructura de objetos que contenga todas las relaciones y atributos necesarios para transmitir información entre los túneles de entrada y salida de la instancia de I2P. La estructura de clases en cuestión, además de ser serializable, debe de ser conocida por todos los “peers” que la utilicen, ya que de no ser así, un emisor puede enviar un objeto Java a un receptor, perfectamente serializado, pero dado que el receptor no cuenta el “.class” de la clase, simplemente no sabrá cómo se debe deserializar el objeto. Lo que normalmente se suele hacer, es distribuir un fichero JAR con todos los elementos necesarios en cada uno de los peers, esto quiere decir que aunque la comunicación entre todos los receptores y emisores en I2P sea descentralizada, tienen que haber elementos de comunicación comunes que les permitan comprender los mensajes que se transmiten por sus túneles, exactamente igual que ocurre con las normas que se definen en cualquier protocolo de red con el fin de garantizar la interoperabilidad en las capas de transporte y aplicación. Además de esto, los receptores y emisores deben conocer los “destinations” de las entidades con las que se desean comunicar. Es posible implementar un mecanismo de descubrimiento automático utilizando un servicio oculto en I2P que sea accesible únicamente por aquellas personas que se desean comunicar y en dicho servicio, cada uno podría depositar el “destination” correspondiente a su instancia de I2P con el fin de que otros usuarios puedan enviarse información.

Para el ejemplo de esta entrada, se creará en primer lugar una clase muy simple que funcionará simplemente como un contenedor de información, también conocido en la terminología de Java como un POJO (Plain Old Java Object) el cual simplemente contendrá algunos atributos básicos y una serie de métodos de establecimiento y acceso (setters y getters)

			
package net.privanon.common; 
public class UserInfo implements java.io.Serializable { 
	private String username; 
	private String name; 
	private String lastname; 
	private Integer age; 
	private String privateMessage;	 
	public void setUsername(String username) { 
		this.username = username; 
	} 
			
	public void setName(String name) { 
		this.name = name; 
	} 
	public void setLastname(String lastname) { 
		this.lastname = lastname; 
	} 
	public void setAge(Integer age) { 
		this.age = age; 
	} 
	public void setPrivateMessage(String privateMessage) { 
		this.privateMessage = privateMessage; 
	} 
	public String getUsername() { 
		return this.username; 
	} 
	public String getName() { 
		return this.name; 
	} 
	public String getLastname() { 
		return this.lastname; 
	} 
			
	public Integer getAge() { 
		return this.age; 
	} 
	public String getPrivateMessage() { 
		return this.privateMessage; 
	} 
} 

La clase en si misma no es demasiado interesante, sin embargo representa la piedra angular del proceso de transmisión de datos entre clientes y receptores, ya que contiene los atributos necesarios para obtener/establecer datos y además, implementa la interfaz Serializable, lo que le permitirá “moverse” libremente entre diferentes JVM.
Modificando un poco la estructura del programa receptor visto en el articulo anterior, el resultado final seria el siguiente.

package testing.i2p; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.OutputStreamWriter; 
import java.io.InputStreamReader; 
import java.io.BufferedReader; 
import java.io.BufferedWriter; 
import java.io.ObjectOutputStream; 
import java.io.ObjectInputStream; 
import java.net.ConnectException; 
import java.net.SocketTimeoutException; 
import net.i2p.I2PException; 
import net.i2p.client.streaming.I2PSocket; 
import net.i2p.util.I2PThread; 
import net.i2p.client.I2PSession; 
import net.i2p.client.streaming.I2PServerSocket; 
import net.i2p.client.streaming.I2PSocketManager; 
import net.i2p.client.streaming.I2PSocketManagerFactory; 
import net.i2p.client.I2PSessionException; 
import net.privanon.common; 
			
public class Server { 
    public static void main(String[] args) { 
        I2PSocketManager manager =
			I2PSocketManagerFactory.createManager(); 
        I2PServerSocket serverSocket =
			manager.getServerSocket(); 
        I2PSession session = manager.getSession(); 
	System.out.println(session.getMyDestination().toBase64()); 
        I2PThread t = new I2PThread(new
			ClientHandler(serverSocket)); 
        t.setName("clienthandler1"); 
        t.setDaemon(false); 
        t.start(); 
    } 
			
    private static class ClientHandler implements Runnable { 
        public ClientHandler(I2PServerSocket socket) { 
            this.socket = socket; 
        } 
			
        public void run() { 
            while(true) { 
                try { 
                    I2PSocket sock = this.socket.accept(); 
                    if(sock != null) { 
			    ObjectOutputStream oos = new
			ObjectOutputStream(sock.getOutputStream()); 
			    ObjectInputStream ois = new
			ObjectInputStream(sock.getInputStream()); 
				UserInfo info = null; 
			    while ((info = (UserInfo) ois.readObject()) != null) { 
				      if(info != null & info.getUsername() != null
			& info.getUsername().equals("Adastra")) { 
						UserInfo response = new UserInfo(); 
				   	        response.setName("unknown"); 
					        response.setLastname("unknown"); 
  					        response.setUsername("unknown"); 
					        response.setAge(0); 
					        response.setPrivateMessage("Send the report
			and close the operation");				 
						oos.writeObject(response);		 
 				       } 
			    } 
			    ois.close(); 
			    oos.close(); 
			    sock.close(); 
                    } 
                } catch (I2PException ex) { 
                    System.out.println("General I2P
			exception!"); 
                } catch (ConnectException ex) { 
                    System.out.println("Error
			connecting!"); 
                } catch (SocketTimeoutException ex) { 
                    System.out.println("Timeout!"); 
                } catch (IOException ex) { 
                    System.out.println("General
			read/write-exception!"); 
                } catch (ClassNotFoundException ex) { 
                    System.out.println("Class Not found
			exception!"); 
                } 
            } 
        } 
        private I2PServerSocket socket; 
    } 
} 

Hay que notar que las principales diferencias se pueden ver en el uso de la clase “UserInfo” que se ha creado anteriormente y la serialización/deserialización de dicho objeto utilizando las clases ObjectOutputStream y ObjectInputStream. En este caso, se deserializa el objeto recibido por el emisor, se realiza una verficación muy simple sobre el nombre del usuario y a continuación, se responde al emisor con un objeto del mismo tipo.

Por otro lado, el receptor por su parte puede tener una estructura como la siguiente:

package testing.i2p; 
import java.io.BufferedReader; 
import java.io.BufferedWriter; 
import java.io.IOException; 
import java.io.InputStreamReader; 
import java.io.InterruptedIOException; 
import java.io.OutputStream; 
import java.io.OutputStreamWriter; 
import java.net.ConnectException; 
import java.net.NoRouteToHostException; 
import net.i2p.I2PException; 
import net.i2p.client.streaming.I2PSocket; 
import net.i2p.client.streaming.I2PSocketManager; 
import net.i2p.client.streaming.I2PSocketManagerFactory; 
import net.i2p.data.DataFormatException; 
import net.i2p.data.Destination; 
import java.io.ObjectOutputStream; 
import java.io.ObjectInputStream; 
import net.privanon.common; 
			
public class Client { 
    public static void main(String[] args) { 
        I2PSocketManager manager =
			I2PSocketManagerFactory.createManager(); 
        System.out.println("Please enter a Destination:");
        BufferedReader br = new BufferedReader(new
			InputStreamReader(System.in)); 
        String destinationString; 
        try { 
            destinationString = br.readLine(); 
        } catch (IOException ex) { 
            System.out.println("Failed to get a
			Destination string."); 
            return; 
        } 
        Destination destination; 
        try { 
            destination = new Destination(destinationString); 
        } catch (DataFormatException ex) { 
            System.out.println("Destination string
			incorrectly formatted."); 
            return; 
        } 
        I2PSocket socket; 
        try { 
            socket = manager.connect(destination); 
        } catch (I2PException ex) { 
            System.out.println("General I2P exception
			occurred!"); 
            return; 
        } catch (ConnectException ex) { 
            System.out.println("Failed to connect!");
            return; 
        } catch (NoRouteToHostException ex) { 
            System.out.println("Couldn't find host!");
            return; 
        } catch (InterruptedIOException ex) { 
            System.out.println("Sending/receiving was
			interrupted!"); 
            return; 
        } 
        try { 
	    ObjectOutputStream oos = new
			ObjectOutputStream(socket.getOutputStream()); 
   	    UserInfo info = new UserInfo(); 
   	    info.setName("Ad"); 
            info.setLastname("Astra"); 
            info.setUsername("Adastra"); 
            info.setAge(30); 
            info.setPrivateMessage("exposure done. Waiting
			for indications"); 
	    oos.writeObject(info); 
	   ObjectInputStream ois = new
			ObjectInputStream(socket.getInputStream()); 
		UserInfo response = null; 
	    while ((response = (UserInfo) ois.readObject()) != null) {
		      System.out.println("Name: "+
			response.getName()); 
		      System.out.println("Last Name: "+
			response.getLastname()); 
		      System.out.println("Username: "+
			response.getUsername()); 
		      System.out.println("Age: "+
			response.getAge()); 
		      System.out.println("Private message: "+
			response.getPrivateMessage()); 
	    } 
	    ois.close(); 
oos.close(); 
	    socket.close(); 
        } catch (IOException ex) { 
            System.out.println("Error occurred while
			sending/receiving!"); 
        }  catch (ClassNotFoundException ex) { 
                    System.out.println("Class Not found
			exception!"); 
                } 
    } 
} 

Nuevamente, las diferencias más destacables entre el programa emisor del articulo anterior y éste, se centran en la serialización/deserializacion de la clase “UserInfo” por medio de los objetos ObjectInputStream y ObjectOutputStream.

Después de iniciar I2P y de ejecutar tanto el emisor como el receptor, se puede ver el flujo de datos transmitidos entre ambas entidades en la siguiente imagen.

streaming1

Aunque ambos programas se ejecutan en la misma máquina y sobre la misma instancia de I2P, el funcionamiento será el mismo en instancias y máquinas separadas.

Otro escenario interesante en el que también se puede utilizar la Streaming Library, es en el desarrollo de plugins que se puedan desplegar directamente desde la consola de administración de I2P. Sobre estas cosas hablaré en un próximo articulo.

Saludos y Happy Hack!
Adastra.


Volver a la Portada de Logo Paperblog