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.
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
- Un thread général qui initialise le proxy et lui permet fonctionner en tâche de fond.
- Un thread à l'écoute du serveur pour recevoir ses informations
- 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.