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 :
- Quelques aspects avancés voire orthogonaux à Java RMI, mais bien utiles comme la sécurité par exemple
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.
- 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)
- 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.
- 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.
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.
- 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.
- 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;
}
}
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.
-
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;
}
}
- Dans le code de lancement du serveur, instanciez une factory de chacun des deux nouveaux types.
- 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
- 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
- créer un keystore à la
racine de votre projet Eclipse, qui contiendra juste un certificat, que
nous créeons en répondant aux questions posées par keytool. En fait,
l'important pour la suite sera que ce keystore existe, et que les 2
extrémités en connaitront l'existence et le mot de passe autorisant à en
lister et voir le contenu (retenez donc bien ce mot de passe !)
$ keytool -genkey -keystore test.ks
Tapez le mot de passe du Keystore :
Ressaisissez le nouveau mot de passe :
Quels sont vos prénom et nom ?
[Unknown] : Francoise
Quel est le nom de votre unité organisationnelle ?
[Unknown] : EPU
Quelle est le nom de votre organisation ?
[UNS] : UNS
Quel est le nom de votre ville de résidence ?
[Nice] :
Quel est le nom de votre état ou province ?
[Unknown] : France
Quel est le code de pays à deux lettres pour cette unité ?
[FR] :
Est-ce CN=Francoise, OU=EPU, O=UNS, L=Nice, ST=France, C=FR ?
[non] : oui
Spécifiez le mot de passe de la clé pour <mykey>
(appuyez sur Entrée s'il s'agit du mot de passe du Keystore) :
- Vérifier que tout s'est bien passé
$ keytool -list -keystore test.ks
Tapez le mot de passe du Keystore :
Type Keystore : JKS
Fournisseur Keystore : SUN
Votre Keystore contient 1 entrée(s)
mykey, 21 avr. 2011, PrivateKeyEntry,
Empreinte du certificat (MD5) : CF:C6:46:E1:90:2B:D2:FE:2E:E9:B3:5A:4E:45:5E:93
- Démarrer l'exécution de
l'application RMI côté serveur requiert de passer deux nouvelles
propriétés Java; indiquant l'emplacement du keystore stockant donc un ou
des certificats, et le mot de passe permettant d'accéder à ce keystore
afin que le serveur puisse prendre le certificat qu'il a besoin de
soumettre au client. Les 2 propriétés sont donc:
-Djavax.net.ssl.keyStore=test.ks -Djavax.net.ssl.keyStorePassword= le mot de passe choisi plus haut
- De même pour démarrer
l'exécution de l'application RMI côté client il faut utiliser une
propriété permettant de localiser le keystore qui est supposé de
confiance, et permettra de valider l'authentification du certificat
reçu.
-Djavax.net.ssl.trustStore=test.ks
Remarque: en déploiement réparti, ce fichier test.ks n'est donc pas
accessible à la fois de la part du serveur ET du client. Pas de soucis,
il suffit
d'en faire une copie sur le site client (ce sera réalisé dans
l'opération préalable de déploiement des codes vers chaque machine
cliente, utiles à la fois au Client pour être programmé puis s'exécuter)
et d'indiquer le bon chemin pour
qu'il y accède afin de vérifier le certificat.
- Pour finir, puisque tout fonctionne, posez vous cette ultime question: comment le client sait-il qu'il utilise une socket Factory basée sur le protocole SSL ? En effet, le client n'a subi aucune modification de son code durant tout cet exercice...
Si vous lanciez votre application en utilisant le serveur http servant
des .class, vous vous apercevriez que la sérialisation dans le
rmiregistry provoque le téléchargement de la classe RMISSLClientFactory.
Le stub que récupèrera le client est équipé de l'information nécessaire
pour que le client se mette à utiliser cette factory de sockets RMI (quitte à ce que, lui
aussi, commence par télécharger le .class correspondant à cette factory qu'il n'avait pas forcément
dans son classpath). C'est la force du système RMI qui permet le téléchargement des .class!
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.
-
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);
}
- 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:
- créer un groupe d'activation pour l'objet remote à activer
- enregistrer ce nouveau groupe auprès du runtime RMI afin d'obtenir un unique identifiant
- créer un descripteur d'activation basé sur le nom de la classe activable et sur cet identifiant
- enregistrer ce descripteur
auprès du runtime RMI afin d'obtenir une instance de stub sur l'objet
activable (qui n'est pas pour autant instancié)
- enregistrer ce stub dans le registre RMI afin que les clients puissent l'utiliser plus tard
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";
}
}
}
- Pour tester, on ne va
donc que lancer la classe de Setup, après avoir démarré le démon rmi
(rmid) sans CLASSPATH. Il faut penser à passer une valeur pour la
propriété codeBase lors du lancement de cette application de Setup. On peut donc, dans l'odre faire ceci (quasi
identique à exo 2 du TP2)
- 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"
- Lancer le rmiregistry
- 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 ?
- 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
- 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-