Applications Réparties - Partie II: approche client/serveur par objets distribués

Cours Polytech'Nice, SI4, 2011-2012, 2013-2014

Sujet de TP3

Objectif/Contenu :


1. Security Manager (rappels?)

Java RMI peut nécessiter l'utilisation explicite d'un SecurityManager afin d'appliquer certaines règles de sécurité. Les règles concernent en particulier ce que le code téléchargé a le droit de réaliser sur le système où s'exécute la JVM (ayant téléchargé du code). En effet, par défaut, on fait confiance au code lu depuis le système de fichier local signifiant qu'une application Java n'est pas protégée par un SecurityManager; cependant il est judicieux de se protéger face au code malicieux qui pourrait nous être envoyé, ce qui peut se produire dans le cadre d'une application RMI. 

Rappel de la syntaxe d'un fichier de policy:

grant [signedBy "signer_names" ,]
  [codeBase "URL"]  // ending with / includes all class files in the specified directory; ending with /* includes all class and jar files in the directory; ending with /- includes all class and jar files in the directory tree rooted at the specified dir.
  [principal  principal_class_name "principal_name", ]+
  {
   [permission permission_class_name ["name",] ["action",]
            [signedBy "signer_names"]  ; ]+
       ...
    };

Exemple: grant codeBase "file:.${/}bin/" { permission java.io.FilePermission "..${/}*", "read" ; };
Ceci a pour effet de donner l'autorisation aux fichiers class qui sont chargés depuis le sous-répertoire bin, à lire les fichiers présents dans le répertoire parent.Rem: ${/} permet d'avoir un séparateur de répertoire portable.

  1. Reprendre la version qui utilisait la sous-classe de Résultat, développée dans le TP2, ainsi que le serveur Http disposant des classes offertes au téléchargement (et donc, le serveur doit être lancé en se servant de la property java.rmi.server.codebase). Dans l'hypothèse où le client devrait télécharger plus d'une classe depuis le serveur Http, expliciter ce que ces classes ont en commun (hint: leur codeBase)
  2. Assurez-vous que le code Client contient bien la création d'un RMISecurityManager (semble-t-il identique à un security manager ordinaire).  Nous allons ensuite établir le fichier de policy le plus restrictif possible étant donné les opérations que le Client veut effectuer. En effet, pour le moment, notre fichier de policy (TP2, Exo 2) est trop permissif, car, il autorise n'importe quel code (grant n'est suivi d'aucune précision relativement à quel code, en fonction de son origine, nous donnons les permissions) à faire tout ce qu'il veut (on a utilisé java.security.AllPermission !!). Pour rendre cet exercice plus sympa, rajouter dans le code du Client, l'écriture de quelques données dans un fichier local. Faites aussi en sorte que la sous-classe de Resultat qui offre une méthode toString, exécute dans le code de toString l'écriture de quelques données dans un second fichier . Testez déjà que tout fonctionne correctement avec le fichier de sécurité le plus permissif possible.
  3.  Les permissions à accorder à la JVM cliente vont donc être scindées en 2 parties (2 "grant"):

    A] le grant donné au codeBase correspondant aux fichiers .class locaux (le code du Client) repéré par une URL de ce type "file:binclient${/}*" (en supposant que les .class coté client sont dans le sous-répertoire qu'on a ici appelé binclient). Dans ce grant, nous devons donner deux types de permission: 1) pour accéder au système de fichier local, et plus précisément au répertoire où le code Client veut écrire;
    2) pour ouvrir des sockets afin de contacter le serveur Http, le rmiregistry et l'objet serveur (qui tourne sur une machine dont l'adresse IP correspondante a été indiquée dans le Stub que le client a reçu. Rappelez-vous que rmiregistry tourne forcément sur la même machine où s'exécute l'objet distant).

    B] le grant donné au code de la sous-classe de Resultat (qui provient de quelle base de code ?)  afin que celui-ci ait le droit d'écrire dans un fichier du système de fichier local.

         Correction

2. Un "Smart" proxy

Lorsque un objet client utilise une référence distante (un proxy/Stub) vers un objet serveur, toutes les méthodes invoquées se traduisent par une communication inter-JVMs. Parfois, on voudrait éviter ceci (si si!!), notamment si la méthode invoquée n'a qu'un but: récupérer une valeur côté serveur mais... qui n'évolue pas durant l'application (typiquement, le nom de l'objet serveur). Admettez que dans ce cas, c'est un peu dommage de payer une communication inter-JVM (parfois, via l'Internet, en tout cas, via un socket TCP/IP), pour rien. L'idée d'un "smart proxy" est d'être "smart", c-a-d de continuer à donner l'illusion à l'appelant qu'il invoque une méthode distante, mais... lui renvoyer la réponse sans avoir besoin de réaliser l'appel distant. Pour celà, le principe est que celui qui aura créé le proxy/stub (correspondant à une référence sur lui-même) aura préparé à l'avance la réponse, qui est donc passée lors de la sérialisation de ce proxy. De plus, la méthode que l'on croit invoquer à distance, via le proxy reçu, aura la même allure que si elle était réellement invoquée à distance, mais, elle récupèrera en fait la valeur déjà disponible localement ("en cache"). Le but de cet exercice est d'illustrer ceci dans la pratique.

  1. Récupérez ici une application RMI. Son principe est assez similaire à d'habitude: un serveur met à disposition un Service à ses Clients (chacun ayant un nom). Les clients contactent le serveur pour récupérer une référence vers le Service. Le client invoque alors une des méthodes qu'offre ce service, et passe en paramètre une référence vers lui-même (un CallBack). Grâce au callback, et après avoir effectué le travail demandé, le serveur envoie une info (ici, une Facture !) au client, et pour sa propre gestion, mémorise qu'il a envoyé une telle facture à ce client. Le client doit donc être accessible à distance afin de pouvoir "recevoir" sa facture (c'est le but du "callback"), et offrir une méthode permettant de récupérer l'identité du Client si besoin. Etudiez finement l'application fournie et testez-là (en local  et sans téléchargement de classes, pour simplifier).Analysez précisément les println correspondant aux appels de la méthode getName offerte par le client.
  2. Le point clé va être de modifier l'interface renvoyée par un Client et qui joue le rôle du callback: elle ne doit pas être java.rmi.Remote, sinon l'appel sur ses méthodes (dont getName) vont forcément engendrer un appel via le réseau. L'objet qui implantera cette interface sera notre "smart proxy":  l'implémentation de cette interface doit par contre faire en sorte de (1) déléguer l'appel, lorsque celui-ci doit être effectué à distance, à la Remote Référence qui représente l'objet distant, mais (2)réaliser l'exécution en local lorsque on sait qu'il n'est pas nécessaire de faire un appel distant. On vous donne ci-dessous le code de la classe pour ce SmartProxy (appelée SmartProxyClient) qui implante justement cette interface non Remote (appelée ItfClient). A vous ensuite d'adapter les fichiers Java fournis en 1.  afin de pouvoir créer, transmettre, utiliser un tel smart proxy. Faites en sorte de minimiser les modifications à apporter dans le code qui utilisait directement une remote référence vers le Client et qui, à présent, doit utiliser le smart proxy.

import java.io.Serializable;
import java.rmi.RemoteException;
public class SmartProxyClient implements ItfClient, Serializable {
    Facturation clientAFacturer = null;
    String clientName=null;
    public SmartProxyClient(Facturation c){
        clientAFacturer = c;
        try {
            clientName=clientAFacturer.getName();
        } catch (RemoteException e) {
            clientName="anonyme";
            e.printStackTrace();
        }
    }
    @Override
    public void facturer(Facture f) throws RemoteException {
        clientAFacturer.facturer(f);
    }
    @Override
    public String getName() throws RemoteException{ // en soi, le throws est inutile... mais obligatoire dans l 'interface ItfClient
        System.out.println("Thread="+Thread.currentThread().getName()+" is calling getName()");
        return clientName;
    }
}
 

     Correction


3. RMI sur SSL (à faire seul, si vous voulez l'utiliser pour le projet)

Rien de plus simple de sécuriser une connexion RMI, en se souvenant qu'une socket TCP (et donc SSL sur TCP) a deux comportements associés (celui côté serveur, celui côté client), et qu'en Java, le design pattern Factory nous permet d'avoir des usines à objets. Alors, lorsque on instancie une classe étendant UnicastRemoteObject, il suffit de lui passer en paramètre du constructeur une instance de deux  Factory permettant de créer les 2 extrémités d'une socket sécurisée.

 

  1. Créez vos 2 Factory. La première implante RMIServerSocketFactory (et Serializable), et la seconde implante RMIClientSocketFactory (et Serializable)


      public class RMISSLServerSocketFactory implements RMIServerSocketFactory, Serializable{
        public ServerSocket createServerSocket (int port) throws IOException{
            SslRMIServerSocketFactory factory= new SslRMIServerSocketFactory();
            ServerSocket socket = factory.createServerSocket(port);
            return socket;
        }
    }

        public class RMISSLClientSocketFactory implements RMIClientSocketFactory, Serializable{

    public Socket createSocket(String host, int port) throws IOException {
    SslRMIClientSocketFactory factory = new SslRMIClientSocketFactory();
    Socket socket = factory.createSocket(host,port);
    return socket;
    }
    }

  2. Dans le code de lancement du serveur, instanciez une factory de chacun des deux nouveaux types.
  3. Passez ces objets Factory en paramètre des constructeurs des classes qui étendent UnicastRemoteObject qui remplacent donc les constructeurs par défaut. Réfléchissez avant de coder : quels sont les objets distants qui doivent utiliser une connexion réseau sur SSL. C'est seulement eux dont l'instanciation (le constructeur) devra faire en sorte d'indiquer ces usines à sockets

  4. Les 2 extrémités d'une socket sécurisée doivent s'authentifier mutuellement afin de pouvoir appliquer ce protocole SSL, cad ouvrir une session SSL; pour l'ouverture d'une telle session, le protocole oblige l'extrémité serveur à fournir un certificat au client qui ensuite, vérifiera sa validité. La solution la plus simple pour cet exercice est d'utiliser un Keystore via un outil fourni en standard par Java, l'outil keytool

       Correction (contenant des screenshots des configurations des JVM client et serveur, sous Eclipse. Notez: pas besoin de lancer le rmiregistry, car c est fait dans le code du serveur).
    Une solution pour sécuriser également l'accès au rmiregistry: le Client, et le Serveur

    4. Les objets distants "Activables" (Activatable (en anglais))
    EXERCICE OPTIONNEL

    Lorsque l'on veut initialiser (instancier) un objet remote seulement au moment où un client veut s'en servir, on se sert du principe d'Activation. Cet exercice va nous aider à découvrir ce principe et le mettre en oeuvre avec l'aide du démon rmi. 

    1. Créez un nouveau projet, en recopiant un des projets RMI précédents qui fonctionnait! L'objet distant qui étendait UnicastRemoteObject va à présent étendre java.rmi.activation.Activatable . Cette classe est similaire à celle qui implantait votre interface Distante , côté serveur, et que vous aviez probablement nommé ObjetDistantImpl. Cette nouvelle classe est quasi identique,  à la différence qu'elle ne comporte pas de méthode main (si tel était le cas, cette méthode servait à instancier cet objet remote et à l'enregistrer dans le RMI registry),  qu'elle étend donc la classe java.rmi.activation.Activatable et qu'elle comporte un constructeur à deux paramètres au lieu du constructeur vide :

        public ObjetDistantImpl(java.rmi.activation.ActivationID id, 
      java.rmi.MarshalledObject data) throws java.rmi.RemoteException {
      super(id, 0);
      System.out.println("ObjetDistantImpl activated with id="+id);
      }

    2. La classe (ou la méthode main) qui auparavant instanciait l'objet distant et l'enregistrait dans le rmi registry va devoir totalement être modifiée. Son nouveau rôle va consister à passer au démon RMI un certains nombre d'information concernant la classe Activatable. Elle s'exécutera donc une fois, et se terminera. Plus tard, si un client a besoin de l'objet distant, alors le démon RMI saura comment activer automatiquement l'objet. Dans cette classe de "setup", il faut procéder aux étapes suivantes, dans l'ordre:
    Le code ci-dessous vous permet de fabriquer une telle classe de setup (remarquez qu'elle aura besoin d'un SecurityManager et donc d'un fichier de policy; elle prend en paramètre le nom utilisé pour l'enregistrement dans le rmi registry)
    public class ActivatableObjetDistantSetup {
    private static final String userDir = System.getProperty("user.dir");
    public static void main(String[] args) {
    try {
    if (args.length < 1) {
    System.out.println("Donner le nom d'enregistrement pour l'objet distant activatable");
    System.exit(1);
    }
    String objDistantName = args[0];
    // Create and install a security manager because the rmid and this JVM will need to communicate
    // Policy file can grant allPermission
    if (System.getSecurityManager() == null) {
    System.setSecurityManager(new java.rmi.RMISecurityManager());
    }
    java.rmi.activation.ActivationGroupDesc objetDistantServerGroup =
    new java.rmi.activation.ActivationGroupDesc(new java.util.Properties(), null);

    // Once the ActivationGroupDesc has been created, register it
    // with the activation system to obtain its ID
    // En fait, rmid lancera une JVM par groupe et y instanciera tous les objets Activatable du groupe
    java.rmi.activation.ActivationGroupID agID =
    java.rmi.activation.ActivationGroup.getSystem().registerGroup(objetDistantServerGroup);

    // The "location" String specifies a URL from where the class
    // definition will come when this object is requested (activated).
    // Cela pourra être notre serveur http habituel!
    String location = System.getProperty("java.rmi.server.codebase");

    // Create the rest of the parameters that will be passed to
    // the ActivationDesc constructor
    java.rmi.MarshalledObject data = null;

    // The location argument to the ActivationDesc constructor will be used
    // to uniquely identify this class; its location is relative to the
    // URL-formatted String, location.
    java.rmi.activation.ActivationDesc descriptor =
    new java.rmi.activation.ActivationDesc(agID,
    ObjetDistantImpl.class.getName(), location, data);

    Distante activableObjetDistant =
    (Distante) java.rmi.activation.Activatable.register(descriptor);
    System.out.println("Got the stub for the Activatable Implementation "+
    "of interface Distante, ie of the remote object implementing Distante");

    java.rmi.Naming.rebind(objDistantName, activableObjetDistant);
    System.out.println("An activable Stub -- not really a real stub on a remote interface! -- on ObjetDistantImpl has \n"+
    "been bound in the RMI Registry under URL ");
    // Un tel stub particulier saura contacter rmid pour lui demander d'instancier la classe qui implémente l'interface Remote
    System.out.println(" rmi://"+getHostName()+"/"+objDistantName);// Indiquer numéro de port après host name si pas = 1099
    // exit
    System.exit(0);
    } catch (Exception e) {
    System.out.println("ActivableObjetDistantSetup err: " + e.getMessage());
    e.printStackTrace();
    }
    }

    private static String getHostName() {
    try {
    return java.net.InetAddress.getLocalHost().getHostName();
    } catch (java.net.UnknownHostException e) {
    return "Unknown";
    }
    }
    }
    1. Lancer le rmid. Pour être sûr que la JVM forkée suite à l'activation aura toutes les permissions pour interagir avec d'autres JVMs (telles rmiregistry ou la JVM cliente), passer l'option -C-Djava.security.policy="fichierpolicy" au moment du lancement de rmid. Dans le fichier de policy, on supposera que toutes les permissions sont données (pour nous simplifier la tache) Ne vous souciez pas du message d'erreur. Au pire, si cela vous inquiète quand même, associez à rmid lui-même un fichier de policy Java. -J-Djava.security.policy=" unfichierpolicy"
    2. Lancer le rmiregistry
    3. Lancer le serveur http (par exemple, sur l'URL localhost:3000) en s'assurant que le répertoire qu'on lui indique contient bien la nouvelle classe Activatable compilée. Pourquoi à votre avis ?
    4. Lancer la classe de Setup avec une ligne de commande du type indiqué ci-dessous  (Setup elle-même a besoin d'un fichier de policy, et aussi d'un security manager . C'est pour interagir avec rmid)
      java -Djava.security.policy=java.policy \
      -Djava.rmi.server.codebase=http://localhost:3000/ \
      ActivatableObjetDistantSetup MonObjetDistant
    5. Lancer un ou plusieurs clients, et allez consulter les messages affichés par le serveur http. Commentez les messages affichés. Si le rmid a été lancé depuis sa propre console, pourquoi voyez-vous des messages ? De quelle JVM proviennent-ils ?




    Page maintenue par Francoise Baude @2011-