RAMM Proxy

Un rélais HTTP concurrent filtrant
 

 Par Daniele Raffo & Matteo Magnani
 

Projet  de "Architecture et programmation réseaux avec Java"
 

Gilles Roussel, Université de Marne la Vallée
 

Mai 2001








Présentation du projet

RAMM Proxy est un relais HTTP concurrent (multithreadé), filtrant (de coté client et de coté serveur), compatible avec la version 1.1 du protocôle, entierement écrit en Java 1.3 et dévéloppé sous Linux. Il est pleinement configurable au moyen des fichiers de configuration in.cfg, out.cfg et proxy.cfg. Un panneau de contrôle permet de verifier à chaque instant l'état du relais et d'y modifier dynamiquement certains paramétres, notamment le nombre de threads qui s'occupent des connections. Le relais effectue aussi une journalisation sur fichier (proxy.log) qui peut être examiné en fonction de différents critéres.

RAMM Proxy est sous le termes de la Licence Publique Générale GNU, version 2.

Le manuel d'utilisation est fourni, en anglais, dans le fichier rammmanual.html. Ce rapport détaille donc seulement les aspects téchniques du projet. Une documentation en Javadoc a été aussi générée.
 

Structure de base du rélais

Le rélais est composé par trois classes principales, et des classes qui répresentent des services, comme démandé du paradigme objet [UML]. Afin d'obtenir un couplage faible et des composants qui s'occupent de taches spécialisées, on a décidé de diviser le rélais en trois modules, comme expliqué dans la figure suivante:

La classe ProxyMain s'occupe de l'interface avec l'usager: elle permet le démarrage du rélais, lit les paramétres eventuels passés en ligne de commande et appelle la classe Configuration éxaminante le fichier de configuration proxy.cfg (si un erreur est engendré à ce niveau, le rélais est demarré également avec des valeurs par défaut).

A un niveau plus bas, il y a un objet Proxy, créé de la classe ProxyMain, qui s'occupe de la gestion du rélais proprement dit (synchronization, journalisation, mise à jour du panneau de contrôle, modification dynamique de la configuration). C'est donc cet objet qui gére les threads qui vérifient et modifient l'état du rélais, threads qui doivent être cachées et pas directement utilisables par l'utilisateur.
Le niveau plus bas est représenté par des objets de type ProxyConnection, qui gérent les connections au niveau du protocole TCP. L'objet de type Proxy  doit seulement créer un objet de ce type, qui puis gére tout seul la création d'autres connections, à l'aide d'un ThreadManager.
Cela implémente le patron Loi de Demeter (aussi connu comme ne pas parler aux inconnus),  et aussi le patron Expert.
 

Gestion des connections - niveau TCP

 
Cela représente le coeur du rélais; il faut bien comprendre que chaque connection serveur-rélais-client est composée en realité de une ou plusieurs sous-connections, chacune rélative à une ressource (fichier html, image, applet...), et chacune gerée par une unique thread. Par exemple, pendant le chargement d'un hypertexte contenant quatre images, un totale de cinq threads sera destiné à ce travail. Donc un MAX_THREADS égal à 20 impliquera l'impossibilité de servir plus que 20 clients au même temps, mais n'engagera pas du tout l'assurance que tous les 20 clients pourront être servis.
Chaque connection, soit chaque thread de la classe Runnable ProxyConnection, ne connait pas directement l'existence d'autres threads. Elle démande donc un service à un objet de type ThreadManager, qui lui dit ce qu'elle doit faire (i.e. attendre une connection, se terminer, ...); l'objet de type ThreadManager est unique pour toutes les connections, donc il possède une vue globale du comportément des threads et permet donc une simple géstion des valeurs de configuration. Avec la méthode setAttributs() il est aussi possible de changer pendant l'execution les bornes pour les threads; à chaque appel de cette méthode, la cohérence des nouvelles valeurs est vérifiée, et eventuellement une IllegalValuesException est lancée - donc l'utilisateur est informé de l'erreur, et le rélais continue à tourner avec les valeurs precédentes.

Gestion des connections - niveau HTTP

Les connections ne travaillent que au niveau TCP. Pour traiter les données au niveau du protocole HTTP elles utilisent des services fournits par des classes qui héritent de la superclasse abstraite HTTPMessageReader. Les classes qui interprétent le flot représenté par une requête ou une réponse HTTP sont, respectivement, HTTPRequestReader et HTTPResponseReader; la méthode qui lit l'header du message HTTP reçoit tous les octets du flot jusqu'aux caractéres « \r\n », répétés deux fois, qui indiquent une ligne vide et donc la fin de l'entête ; ensuite, cette méthode fait un parsing pour récuperer la valeur du Content-Length (obligatoire en HTTP/1.1) et alloue un tampon de telle taille pour reçevoir le reste du flot (le corps du message). Ce dernier est lu au moyen de la méthode readFully(). Pour compatibilité avec HTTP/1.0, si Content-Length n'est pas indiqué, on alloue un tampon suffisament grand (1024 Kbytes) à stocker le flot ; dans ce cas, la fin du message est marquée par la fermeture de la connexion.

Les machines de l'UMLV sur lesquelles on a travaillé (10.x.x.x) étant derriére un proxy qui transforme le HTTP/1.1 en 1.0, on a testé le protocôle en se connectant à ryu.univ-mlv.fr - un serveur intérieur implémentant le 1.1. De toute façon, notre proxy est aussi compatible avec le protocole 1.0.

Chaque connection, grâce aux classes HTTP*Reader, est capable de savoir si elle doit se terminer après le premier echange de données, ou si elle doit attendre un autre message de la coté serveur ou client: un entête du type 1xx signifie que le serveur doit encore envoier la vrai réponse, et une commande "Connection: keep-alive" informe la connection qu'elle doit rester ouverte.
 

Filtrage

 
Le filtrage est realisé au moyen d'un objet AccessVerifier, qui lit un fichier de filtrage (in.cfg ou out.cfg) et réponds aux questions si un certain client ou serveur doit être accepté ou pas. Une UnrecognizedCfglineException est lancé si les fichiers de filtrage contiennent de commandes incompréhensibles.
Des pages HTML sont produites si la requête du client est refusée.
Par exemple, si on veut interdire à un client de joindre le site defendu "www.microsoft.com", voici la ligne du fichier de configuration qui provoque ce filtrage:
deny www.microsoft.com
Même si dans l'exemple le site interdit est un site particulier, on a donné à l'administrateur du rélais aussi la possibilité d'utiliser des meta-caractéres (* et ?).
On n'a pas utilisé le paquetage gnu.regexp car les expressions régulières utilisent le metacaractére point (.) pour répresenter « n'importe quel caractére », et donc les fichiers de filtrage auraient du être écrits en déspecialisant chaque occurrence du caractére point (par exemple www\.java\.sun\.com), ce qui n'est pas trés lisible. Pour une utilisation si légére des expressions régulieres telle qu'on en fait dans les fichiers de filtrage, on a preferé écrire from scratch nos propres simples méthodes d'evaluation.
 
 
Basic authorization
 
Le filtrage, qu'on vient de voir, permet seulement de accepter ou pas une requête; on a voulu donner plus de choix à l'administrateur, en lui permettant de protéger des ressources au moyen d'une couple usager/mot-de-passe. Pour faire cela, on a utilisé l'option d'authorization Basic.
Un fichier de configuration "aut.cfg" contient une liste de ressources (url) avec de suite les couples usager/mot-de-passe codées en base 64; naturellement, ce fichier ne doit pas être accessible en lecture!
Un objet de type AuthorizationVerifier lit ce fichier au démarrage, et est en suite utilisé pour verifier 1) si une ressource est protégée 2) si un mot de passe pour cette ressource est valable 3) pour envoyer des messages de warning.
Quand un client demande une ressource, l'algorithme suivant est utilisé:
1) vérifier si l'url figure dans la liste des urls protégés
1.1) si non, continuer l'éxécution.
1.2) si oui, -> 2
2) vérifier si dans la requête il y a une commande Authorization: Basic xxxx....
2.1) si non, envoyer une réponse avec comme entête WWW-Authorization: Basic realm: realm et sortir
2.2) si oui, -> 3
3) vérifier si xxxx... correspond à une entrée dans le fichier aut.cfg pour l'url démandé.
3.1) Si non, afficher une page 404 FORBIDDEN
3.2) Si oui, continuer l'éxécution.
Si on veut joindre une ressource protégée, le browser ouvre une fenêtre pour envoyer le mot de passe et le userid.
 
Un fichier aut.cfg doit avoir le format du fichier qui suit:
url: www.voyez.com
ZHVyaXM6ZHVyaXM=
bWFnbmFuaTptYWduYW5p
cmFmZm86cmFmZm8=
:lru
url: www.microsoft.com
bWFnbmFuaTptYWduYW5p
cmFmZm86cmFmZm8=
ZHVyaXM6ZHVyaXM=
:lru
 
 
Journalisation (logging)
 
La classe LogfileManager permet de définir un fichier de log, d'y écrire une ligne corréspondante à une entrée, et de générer des fichiers HTML contenants le log trié selon différents critères. Cette classe est indépendante; elle lit le fichier de log, mémorise le contenu dans quatre ArrayList (pour le client, date, requête et résultat), effectue le tri et écris les fichier HTML corréspondants. Le tri est fait à la main (on n'a pas utilisé un Comparator) selon un selection sort, car en triant les élements d'un ArrayList on a la nécessité de bouger chaque élément corréspondant des trois autres.
La méthode qui écrit une entrée dans le fichier de log ferme le fichier aprés chaque écriture, pour éviter d'avoir un fichier de log toujours ouvert pendant que le proxy tourne.
 
 
Supervision à distance

La supervision du proxy est effectué au moyen d'un panneau de contrôle control-panel.html, construit à la volée par la classe ControlPanel.

Ce panneau contient les informations rélatives au proxy (serveur, port et horaire de démarrage), aux threads (valeurs de MIN_THREADS, MAX_THREADS, MIN_SPARE_THREADS, MAX_SPARE_THREADS et leur état) et des liens aux fichiers de configuration et au log trié.
 

Le panneau est mise à jour tous les CONTROLPANEL_UPDATE_DELAY seconds par un procéssus léger démarré par la classe Proxy.
 

Configuration à distance et à la volée via HTTP

 
La configuration à distance du relais est réalisable à partir d'un form, accéssible à partir du panneau de contrôle en cliquant sur le lien « Change configuration ».
Il est donc possible de changer à distance les valeurs des bornes pour les threads, et aussi d'arreter le rélais; cela est géré par un script CGI écrit en perl et activé au moyen de la méthode POST. Le script (form.cgi) s'occupe des taches suivantes:

- parsing des informations obtenues par la méthode POST dans la variable ENV
- vérification du bon format des données
- génération d'une page HTML de réponse pour l'usager (positive ou negative)
- si les données sont valables, écriture dans le fichier "proxy.tmp" des données

Voici le fichier "proxy.tmp" après l'éxécution de "form.cgi":

#----Config. file automatically generated by form.cgi----
MinThread 8
MaxThread 50
MinSpareThread 8
MaxSpareThread 10

Ce fichier constitue l'interface entre la CGI (donc le web) and le programme Java:  un procéssus léger, nommé "timer" et lancé par la classe Proxy, contrôle si l'utilisateur a modifié le fichier (à partir de la date de dernière modification) et, dans ce cas là, change les valeurs des threads et demande si nécessaire l'arrêt du rélais.

Il faut noter que la CGI vérifie la cohérence des donnés, mais elle ne connait pas les impostations du rélais au moment de la requête via form, donc c'est par exemple possible de démander une valeur de max-threads = 20 pendant que le rélais est en train de servir 25 connections. Cela ne pose pas de problèmes, parce que la classe ThreadManager permet de gérer aussi des cas de ce type, et dans l'exemple tue le premières 5 connections qui se terminent, sans créer de nouvelles threads en attente.

A cause de l'impossibilité de tester la CGI à l'interieur de l'université, ou elles sont interdites pour sécurité, le code disponible dans le répertoire "src" a été modifié en ce qui concerne:
- la lecture des données en entrée, qui est fait sur l'entrée standard et n'utilise pas la variable ENV
- l'écriture de la page web de réponse, qui n'est pas faite sur la sortie standard mais dans un fichier "output.html"
Cela permet de tester la CGI en local, en tapant sur le prompt du shell:

$ form.cgi <input

où "input" est un fichier qui contienne les mêmes données contenues dans la variable ENV après envoi du form, dans l'exemple qu'on a utilisé:

MinThread=8&MaxThread=50&MinSpareThread=8&MaxSpareThread=10

On pourra lire la sortie dans le fichier output.html. Dans le cas où l'arret est démandé, la thread "timer" de la classe Proxy cause la fin du programme (i.d. la sortie d'une boucle infinie d'attente) et donc aussi des threads qui gérent les connections, qui se terminent tout de suite.
 

Conclusion

Le développement de ce projet nous a donné un premier aperçu sur le protocôle HTTP, trés connu à un niveau pratique (qui n'a jamais surfé sur le Web ?) mais qui, à un niveau d'implementation, présente des problèmes pas du tout triviaux et ouverts (nouvelles versions du protocôle, compatibilité avec les différents clients tels que Netscape ou IE...).
Ce projet nous a permis de nous confronter avec les problemes qu'on incontre quand on doit implementer des concepts, parfois élémentaires à un niveau theorique, mais qui présentent des difficultés materielles. Notre simple proxy, même présentant certaines failles et manquances d'optimisations, a pourtant un interêt didactique; en fait, par l'examination des points non traités et des erreurs découverts pendant la soutenance, nous avons appris des concepts aussi importants que ceux que nous avons appris en écrivant le code.