Réalisation d'un Proxy en Java

LordDaedalus

Cet article présente la réalisation d'un proxy en java. Le proxy est un logiciel d'interface qui reçoit la requête du client, la transmet au serveur, attend la réponse du serveur avant de la transmettre au client.

- Titre : Réalisation d'un Proxy en Java

- Auteur : LordDaedalus

- Parution : 21 février 2009

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Serge Ungar. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Présentation du contexte

- II. Principe de fonctionnement

- III. Les threads

- IV. Le protocole HTTP

- IV-A. Principe

- IV-B. La requête du client

- IV-C. La réponse du serveur

- IV-D. Remarque à propos du POST

- IV-E. Remarque sur l'adresse du site :

- V. Les sockets

- V-A. Coté Client

- V-B. Coté Serveur

- VI. Les flux

- VII. En terme de codage Java

- VII-A. Initialisation du proxy

- VII-B. Le Thread Serveur

- VII-C. Le Thread coté Client

- VII-C-1. Initialisation des flux

- VII-C-2. Analyse de la requête:

- VII-C-3. Envoie de la requête au serveur

- VII-C-4. Attente de la réponse du serveur:

- VII-D. Le Thread EnvoiServeur

- VIII. Remarques

I - Présentation du contexte

De nombreux systèmes informatiques utilisent des proxys pour permettre l'accès à Internet. Les 3 principales raisons de ce choix sont :

- Assurer la sécurité en filtrant les entrées/sorties des flux Internet

- Filtrer les requêtes émises par l'utilisateur

- Assurer un suivi des échanges par la réalisation d'un journal des requêtes.

Le proxy est donc un logiciel d'interface qui reçoit la requête du client, la transmet au serveur, attend la réponse du serveur avant de la transmettre au client.

Le proxy s'appuie sur les protocoles Internet de la couche Application et doit pouvoir gérer les protocoles HTTP, FTP, SMTP, POP3, ? afin de ne pas bloquer l'utilisateur dans son utilisation quotidienne.

II - Principe de fonctionnement

La présentation du fonctionnement est faite en s'appuyant sur le protocole http qui permet de surfer sur Internet à travers un navigateur comme IE ou Mozilla.

Le protocole HTTP stipule que la requête doit se faire sur le port 80. Afin d'assurer sa fonction d'interface, le proxy va demander au navigateur d'émettre sa requête et de recevoir les données sur le port 8080, lui en tant que proxy se chargera de transférer au serveur la requête sur le port 80. Puis, il se mettra ensuite en attente sur ce port et dès la réception des données du serveur il transférera au navigateur les informations sur le port 8080.

Principe général d'un proxy

En fait, seul le port 80 est figé car associé à un protocole. Pour le dialogue entre le navigateur et le proxy, il n'y a pas de règle quant à la spécification du port si ce n'est qu'il ne faut pas prendre un port associé à un protocole (comme le port 80 pour HTTP ou 21 pour FTP). Il existe une liste des ports référencés dans la RFC 3232. Habituellement pour les proxy on utilise un port de la famille 8000 à 8100 dont le plus connu est le port 8080, on aurait tout aussi bien pu utiliser le port 8008 ou un autre.

III - Les threads

Le proxy doit fonctionner d'une façon transparente pour l'utilisateur. Les threads sont donc parfaitement adaptés à ce type de fonctionnement. On a besoin de 3 threads

1. Un thread général qui initialise le proxy et lui permet fonctionner en tâche de fond.

2. Un thread à l'écoute du serveur pour recevoir ses informations

3. Un thread à l'écoute du client pour recevoir les requêtes

IV - Le protocole HTTP

IV-A - Principe

Les règles de fonctionnement du protocole sont décrites dans la norme RFC 1945 - HTTP 1.0. Il s'agit de transférer des données localisées par une adresse URL, à la requête d'un client à partir d'un serveur. L'échange se fait en deux temps :

- Le client fait une demande au serveur (requête)

- Le serveur renvoie les données au client (réponse)

IV-B - La requête du client

La requête est une ligne de commande en ASCII envoyée au serveur qui comprend :

- Une ligne de requête

- Les champs d'entête de la requête

- Le corps de la requête

La ligne de requête est constituée de la méthode devant être appliquée, de l'URL de destination et de la version du protocole.
Par exemple :

GET http://www.google.fr HTTP 1/0

Elle est envoyée en ASCII et terminée par CRLF.

Les champs d'entête contiennent des informations complémentaires envoyées au serveur comme le nom du navigateur, le système d'exploitation, ...Chaque ligne est composée du nom de l'entête suivi de deux points (:) et de l'information. Elles sont terminées par CRLF.

Le corps de la requête est détaché des champs d'entête par une ligne vide. Il est constitué d'un ensemble de lignes terminées par CRLF et permettant de transférer des informations au serveur comme des cookies ou des informations provenant d'un POST.

Les commandes de la requête sont :

- GET : Demande les informations concernant l'URL au serveur

- HEAD : Demande les entêtes de l'URL au serveur

- POST : Envoie des données au programme associé à l'URL sur le serveur

- PUT : Permet de charger un fichier sur le serveur

- DELETE : Permet de supprimer un fichier du serveur

- CONNECT : Autorise le client à utiliser le serveur comme un Proxy

- TRACE : permet de connaître les commandes qui ont été envoyées au serveur

Voici un exemple de requête GET envoyée pour obtenir la page www.google.fr :

- GET http://www.google.fr HTTP /1.0

- User-Agent=Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11

- Accept=text/xml,application/xml,application/xhtml+xml,text/html

- Accept-Language=fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3

- Accept-Encoding=gzip,deflate

- Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.7

- Keep-Alive=300

- Connection=keep-alive

- Cookie=PREF=ID=e51c0f613f6108fc:TM=1197017791:LM=1197017791

IV-C - La réponse du serveur

Elle commence toujours par une ligne comprenant :

- Le protocole utilisé

- Le statut de la requête

- La signification du statut:

Par exemple :
HTTP/1.0 200 OK

Elle est suivie des champs d'entête de la réponse puis du corps de la réponse. Par exemple, dans le cas précédent de www.google.fr, on obtient :

- HTTP/1.0 200 OK

- Cache-Control : private

- Content-Type : text/html; charset=UTF-8

- Content-Encoding : gzip

- Server : gws

- Content-Length : 2546

- Date : Thu, 10 Jan 2008 09:40:16 GMT

Suivi du corps de la page.

IV-D - Remarque à propos du POST

Dans le cas présent seul les commandes de requête GET et POST font nous interesser. La commande POST à une particularité : Afin de transmettre les informations par exemple d'un formulaire, elle se présente comme une requête GET (don on a remplacé GET par POST) avec à la fin une ligne de données supplémentaires qui sont les données du formulaire à transmettre :
Voici un exemple de requête POST envoyée:

- POST http://www.google.fr HTTP /1.0

- User-Agent=Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11

- Accept=text/xml,application/xml,application/xhtml+xml,text/html

- Accept-Language=fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3

- Accept-Encoding=gzip,deflate

- Accept-Charset=ISO-8859-1,utf-8;q=0.7,*;q=0.7

- Keep-Alive=300

- Connection=keep-alive

- Login=toto&Password=motdepasse&submit=envoyer

On remarque que les données sont séparées par un saut de ligne (CRLF).

IV-E - Remarque sur l'adresse du site :

Quand on envoie une demande au serveur pour accéder à un site, l'adresse du site par exemple www.developpez.com est inclus dans la requête. Il va donc falloir extraire cette adresse pour définir à quel serveur on s'adresse.

V - Les sockets

V-A - Coté Client

Le proxy écoute le client sur le port local 8080, il y a donc une socket pour le serveur et une socket pour le client coté client :

- ServerSocket server = new ServerSocket(8080);

- Socket client = server.accept();

La socket client accepte la connexion demandée par le serveur.

V-B - Coté Serveur

Le proxy écoute le serveur sur le port 80, il y a donc une socket pour le serveur avec l'adresse du site et le port d'écoute, soit :

- Socket socketServer = new Socket("http://www.developpez.com",80);

Il est important de donner le nom de domaine à la socket pour permettre la résolution des noms de domaine (DNS) et donc trouver le bon serveur où est installé le site et de préciser le protocole via le port 80 pour lui spécifier que c'est une requête http.

VI - Les flux

Afin d'assurer le transfert des requêtes du client vers le serveur et des informations du serveur vers le client, on a besoin de 4 flux vu du coté du Proxy :

- ClientIn qui envoie la requête au proxy (la requête «entre» dans le proxy d'où le «in»)

- ServerOut qui transfert le requête reçu du client au serveur

- ServerIn où le proxy reçoit la réponse du serveur à la requête du client

- ClientOut où le proxy renvoie les informations du serveur au client.

VII - En terme de codage Java

VII-A - Initialisation du proxy

Le proxy devant fonctionner en tâche de fond afin d'être en permanence à l'écoute du navigateur et du serveur il est initialisé via un thread dans le programme principal :

ProxyServerThread proxyServerThread = new ProxyServerThread();
Thread thProxyServer = new Thread(proxyServerThread);
thProxyServer.start();

VII-B - Le Thread Serveur

Il est à l'écoute du navigateur. On commence par initialiser l'écoute sur le port local. Ensuite, le thread se met en attente d'une interruption venant du navigateur. Quand cette interruption à lieu il initialise à thread dédié au client.

ServerSocket server = new ServerSocket("8080");
while (!interrupted())
{
Socket client = server.accept();
ProxyServerClientThread proxyServerClientThread = new ProxyServerClientThread(client);
Thread thProxyServerClient = new Thread(proxyServerClientThread);
thProxyServerClient.start();
sleep(5);
}

VII-C - Le Thread coté Client

Le thread Client est le Thread principal qui a plusieurs fonctions majeures :

- Initialise tous les flux

- Il analyse la requête pour en définir le type (GET ou POST) et trouver le nom de domaine

- Il envoie la requête au serveur

- Il attend la réponse du serveur pour la transférer au navigateur

VII-C-1 - Initialisation des flux

Les différents flux sont exploités sous forme de DataStream ce qui donne :

DataInputStream clientIn, serverIn;
DataOutputStream clientOut, serverOut;

Les sockets renvoient des bytes qui sont récupérés via les méthodes getStream avant d'être bufférisés pour pouvoir être exploités :

clientIn = new DataInputStream(new BufferedInputStream(client.getInputStream()));
clientOut = new DataOutputStream(new BufferedOutputStream(client.getOutputStream()));
serverIn = new DataInputStream(new BufferedInputStream(socketServer.getInputStream()));
serverOut = new DataOutputStream(new BufferedOutputStream(socketServer.getOutputStream()));

VII-C-2 - Analyse de la requête:

Deux aspects majeurs sont à prendre en compte. D'une part on ne connait pas à priori le nombre de byte constituant la requête ni son type. D'autre part la lecture d'un flux détruit le flux, il va donc falloir reconstituer la requête du serveur avant de la transmettre car elle aura été détruite lors de son analyse.
La requête du serveur va donc être lue byte à byte, chacun des bytes étant rnager sous forme de caractères dans une String (ligne):

DataInputStream clientInSave = new DataInputStream(new BufferedInputStream(clientIn));
String ligne = "";
String carac = "";
int bytesLu;
byte[] request = new byte[1];
int nbrBytes = clientInSave.available();
for (int index = 0; index < nbrBytes;index++)
{
bytesLu = clientInSave.read(request);
carac = Character.toString((char)request[0]);
ligne = ligne + carac;
}

Cette chaine de caractère sera splittée sur le critère de fin de ligne/retour chariot de la norme http. Chacun de ces lignes étant rangée dans un tableau :

requete  = new ArrayList<String>();
String[] tabLigne = ligne.split("\r\n");
int nbrLigne = tabLigne.length;
for (int index = 0; index < nbrLigne;index++)
requete.add(tabLigne[index]);

Il ne reste plus qu'à analyser chacune des lignes pour en extraite le type de requête GET ou POST et le nom de domaine qui sert à générer la socket de connexion avec le serveur :

String strUrl = requete.get(0);
if (strUrl.startsWith("GET"))
{
String sub = strUrl.substring(4);
int posFin = sub.indexOf("HTTP");
sub = sub.substring(0,posFin);
urlSend = TrouveUrlBase(sub);
}

if (strUrl.startsWith("POST"))
{
String sub = strUrl.substring(5);
int posFin = sub.indexOf("HTTP");
sub = sub.substring(0,posFin);
urlSend = TrouveUrlBase(sub);
}

URL url = new URL(urlSend);
socketServer = new Socket(url.getHost(),80);

VII-C-3 - Envoie de la requête au serveur

Pour ne pas bloquer l'application on envoie la requete au serveur à travers un Thread. On y passe deux paramètres la requete elle-même et le flux de sortie vers le Thread, la socketServer ayant été initialisée au prélable ce qui permet de générer le flux de sortie (serverOut) :

ThreadEnvoiServer threadEnvoiServer = new ThreadEnvoiServer(serverOut,requete);
Thread thEnvoiServer = new Thread(threadEnvoiServer);
thEnvoiServer.start();

VII-C-4 - Attente de la réponse du serveur:

Tous les paramètres et les flux ayant été initialisé, le Thread se met en attente de la réponse du serveur qu'il renverra dès sa réception :

byte[] reply = new byte[4096];
int bytesRead;
while ((bytesRead = serverIn.read(reply)) != -1){
clientOut.write(reply, 0, bytesRead);
clientOut.flush();
}

VII-D - Le Thread EnvoiServeur

Afin de ne pas pénaliser l'application, on créé un Thread qui envoie les données au serveur ce qui évite de bloquer l'ensemble le temps d'un envoi :

PrintWriter out = new PrintWriter(new OutputStreamWriter(serverOut));
for (int i = 0; i < requete.size(); ++i)
out.println(requete.get(i));
out.println(); // Envoyer une ligne vierge -> fin de la requête
out.flush();

VIII - Remarques

- On aurait également pu faire un thread pour l'attente de la réponse mais cela alourdissait considérablement l'exemple par la gestion de la synchronisation des threads

- Il ne faut pas oublier également toute la gestion des exception qui permet de gérer les fermetures des flux et des sockets. Elle est présentée dans le code général fournis en exemple.