dimanche, 8 juin 2008

Implémentation d'un serveur proxy HTTP

La question à la base de cet article est la suivante :

Comment faire pour que mes concurrents ne soient pas au courant que j'observe (très) régulièrement une section particulière de leur site internet ?
La première étape bien évidemment est de la légitimer cette question. Comment un concurrent pourrait savoir que je consulte régulièrement son site ?

La réponse est simple. Prenons un cas concret pour illustrer : ELCA Informatique possède les plages d'adresses ips 193.72.144.0 - 193.72.147.255 (bloc /22 = 1024 ips, hosté chez Cablecom) et 193.73.238.0 - 193.73.238.255 (bloc /24 = 256 ips, hosté chez Sunrise)

Ce genre d'info se trouve très facilement via un mélange whois, traceroute et autres informations DNS.

Une fois qu'on a ces ips, il suffit de rechercher dans les logs les hits faits par ces ips et on pourra dresser un profil précis des intérêts des collaborateurs d'ELCA sur notre site.

Donc comment faire pour éviter cette traçabilité ?

Connexion Internet à adresse ip dynamique (xDSL)

Une première solution est d'utiliser une connexion xDSL pour toutes les connexions humaines à Internet, et de dédier exclusivement les plages d'ips fixes et repérables aux services de l'entreprise. Cette pratique est de plus en plus courante, surtout avec l'arrivée des connexions VDSL et de boitiers qui permettent d'agréger plusieurs lignes xDSL afin d'une part d'avoir un redondance, et d'autre part de partager la somme des débits des lignes entre les utilisateurs. Le seul détail concernant cette solution est de vérifier que l'adresse ip change réellement, faute de quoi il faudrait forcer ce changement.

Serveur proxy

Une autre solution, que je vais détailler ici car elle est plus intéressante d'un point de vue ingénierie informatique, est de faire passer le traffic des utilisateurs par un proxy. Par proxy j'entends tout type de connexion indirect à Internet, donc autant les serveurs proxy HTTP, SOCKS, etc que les réseaux de proxy comme par exemple Tor.
Le fait d'accéder à Internet par l'intermédiaire d'un proxy permet de visualiser un site avec une adresse ip qui n'est pas notre, mais appartient à un prestataire de ce genre de service. Cependant l'anonymité à 100% n'existe pas : il existera toujours un intermédiaire qui saura qui a accédé où. La confiance des intermédiaires est donc de mises dans cette configuration.

Il existe une grande quantité de serveur proxy logiciel de qualité, open source ou commercial, comme par exemple Squid, Privoxy, Apache mod_proxy, etc... Mais une fois de plus, l'utilisation d'un logiciel existant nous priverait du plaisir de développer de notre propre implémentation.

Principe de base

Le principe d'un serveur proxy est donc d'intercepter la requête d'un navigateur, d'ouvrir une connexion vers le site cible, transmettre la requête original, lire le résultat et finalement renvoyer ce résultat au navigateur.

Protocole HTTP

La partie centrale d'un serveur proxy HTTP réside sa compréhension du protocole HTTP. Celui-ci comporte des particularités qui vont être détaillées.

Le serveur proxy va recevoir une requête du navigateur, dont la première ligne comporte l'action à exécuter, l'url de la cible et une éventuelle version du protocole utilisé :
GET http://www.noisette.ch/ HTTP/1.0
Cette première ligne va donc directement nous renseigner sur le serveur cible à connecter. Le reste de la requête est composée de lignes de header, contenant diverses informations sur la requête comme le navigateur utilisé ou le referer (en français on dit comment ?).

Transfer de la requête au serveur cible

La prochaine étape est le transfert de la requête au serveur cible. A ce moment on peut éventuellement modifier ou enlever des headers du client, voir en ajouter d'autres. Plus précisément, il peut être intéressant de filtrer tous les headers des clients qui pourraient trahir l'utilisateur du proxy (information leakage).
Reste la lecture du résultat, qui n'est de loin pas la partie la plus triviale.

Ne pas bufferiser la lecture du résultat

L'implémentation d'un serveur proxy dans laquelle on lit le résultat en entier (via un lib du style httpclient) avant de l'envoier ensuite au navigateur n'est à mon avis pas bonne : le délai entre la requête et la réponse risque d'en prendre un coup, surtout pour les grosses pages.
L'idée est donc de retourner directement les bytes lus du serveur web au client.

En pseudo code, ca donne quelque chose du style :
 // fromWeb : InputStream du socket de la connexion vers le serveur source
// toBrowser : OutputStream du socket de la connexion vers le browser
while ((i = fromWeb.read(buffer)) != -1) {
toBrowser.write(buffer, 0, i);
toBrowser.flush();
}

Fermeture des connexions

La fermeture des connexions est très importante afin de libérer les ressources et ne pas avoir un serveur qui explose après 20 requêtes. Facile en théorie, la fermeture de la connexion est la sous partie non triviale. Dans la mesure où le serveur ne ferme pas systématiquement la connexion à la fin du transfert si il reçoit un header "Connection: keep-alive", il faut détecter quand la réponse du serveur est complète pour pouvoir fermer la connexion.

Dans une grande majorité des réponses, la taille de la réponse est spécifiée via un header "Content-Length: 82". Il suffit donc de lire autant de bytes que nécessaire et la connexion peut être fermée. Mais quelques réponses ne vont pas retourner de taille. C'est le cas par exemple lors du streaming d'un fichier. Dans ce cas il faut lire la réponse jusqu'à ce que le serveur ferme lui-même la connexion.

Transfer-Encoding: chunked

Une autre difficulté à la lecture de la réponse est l'encodage chunked. Dans ce mode de transfert, le serveur sépare la réponse en plusieurs parties, les chunks. Ca lui permet d'envoyer une partie de la réponse tout en calculant la suite. Par exemple Google utilise ce mode de réponse : les 2-3 premiers résultats sont dans un cache direct, et Google les retourne dans un premier chunk. Puis il doit aller chercher les 7-8 autres dans un deuxième cache, opération un peu plus coûteuse, qui se fait pendant que le navigateur du client lit ce premier chunk. Ca permet au serveur de ne pas attendre d'avoir entièrement cherché les 10 résultats, et de profiter du temps de transfert avec le client. Du point de vu du client, une fois qu'il a fini de lire le premier chunk, les suivants sont prêts à être lus. L'impression de fluidité est donc parfaite. Mais malheureusement peu d'implémentations tirent intelligemment profit de ce type de transfert.

En pratique, la réponse d'une encodage chunked donne quelque chose du style :
taille du chunk en hexa \r\n
données \r\n
taille du prochain chunk \r\n
données \r\n
...
0\r\n
\r\n
Pour lire la réponse, il faut donc lire la taille du chunk, lire les données du chunk, lire la taille du prochain chunk, etc jusqu'à ce qu'on ait un chunk de taille null, signifiant la fin de la réponse.
HTTP/1.1 200 OK¶
Date: Fri, 31 Dec 1999 23:59:59 GMT¶
Content-Type: text/plain¶
Transfer-Encoding: chunked¶

1a¶
abcdefghijklmnopqrstuvwxyz¶
10¶
1234567890abcdef¶

Caching

Prochaine étape dans l'implémentation d'un serveur de cache utile est efficace, c'est l'ajout d'une couche de cache au niveau du proxy : le serveur cible va retourner une information sur la date de dernière modification de la page. Cette indication peut être utilisé pour retourner la page mise en cache plutôt que de relire la réponse du serveur.

Authentification

Dernier point important si on met un serveur proxy sur internet, il faut ABSOLUMENT implémenter un ACL (access control list) afin de restreindre l'accès et l'utilisation du proxy aux personnes authentifiées uniquement. Faute de quoi votre tout beau serveur proxy sera vitre trouvé et utilisé par des personnes malveillantes pour spammer, consulter des sites illégaux etc.

Prochaine étape : au travail !

Aucun commentaire: