Le but de ce TP est de programmer en Perl un proxy HTTP.

Il s'agit du TP proposé l'année dernière par Antoine Miné. La correction est disponible sur la page d'Antoine.

Fonctionnement d'un proxy HTTP

Un proxy (aussi appelé serveur mandataire) est un serveur capable de relayer des pages WEB entre un client et un serveur HTTP.

Au lieu de se connecter directement au serveur indiqué par l'utilisateur, le client se connecte au proxy. Celui-ci contacte le serveur réel, télécharge la page demandée et la retransmet au client. Vu du côté du client, le proxy agit comme un serveur HTTP. Vu du côté du serveur, le proxy agit comme un client HTTP.

Quand le client et le serveur sont séparés par un pare-feu qui interdit toute connection directe, l'emploi d'un proxy (qui tourne sur le pare-feu) est indispensable.

Généralement, un proxy ne se contente pas de relayer de façon transparent toutes les requêtes et les réponses HTTP. Parmi les utilisations possibles d'un proxy HTTP on compte:

  • mettre en cache les pages les plus fréquemment demandées,
  • faire des statistiques sur les pages accédées,
  • filtrer l'accès à certaines pages WEB,
  • imposer à l'utilisateur de s'identifier,
  • protéger l'utilisateur en rendant anonymes les connections (e.g.: en maquillant les en-têtes HTTP, en supprimant les cookies, en routant les requêtes à travers plusieurs proxy successifs),
  • établir un tunnel qui compresse les transactions HTTP, les crypte (e.g.: SSL) ou les convertit dans un autre protocole,
  • modifier les pages WEB à la volée.

Protocole de proxy

Le protocole HTTP supporte explicitement l'utilisation de proxys grâce à des en-têtes dédiées. Une requête d'un client à un proxy ressemble à ceci:

GET http://www.di.ens.fr/~mine/enseignement/syst2006/index.html HTTP/1.1
Host: www.di.ens.fr
Proxy-connection: keep-alive
autres en-têtes
...
ligne vide

On note les différences suivantes avec une requête classique HTTP 1.1:

  • GET spécifie toujours l'URL complète de la page, y compris le nom de protocole (http://) et le nom de serveur (www.di.ens.fr),
  • l'en-tête Host indique le nom (et éventuellement le port) du serveur de la page recherchée, différent du nom du serveur proxy contacté,
  • l'en-tête facultative Proxy-connection est utilisée à la place de Connection pour indiquer si le serveur doit maintenir la connection ouverte après avoir servi la page (keep-alive, par défaut si l'en-tête est absente) ou la fermer (close).

On rappelle que les en-têtes sont insensibles à la casse, que les lignes sont toutes terminées par \r\n et que la fin de l'en-tête est indiquée par une ligne vide.

Référence: le protocole HTTP 1.1 est décrit dans la RFC 2616. Voir également les explications du TP précédent. top

Exercice 1

Avant toute chose, modifiez les préférences de votre navigateur préféré pour qu'il utilise comme proxy localhost sur le port 8080.

Par exemple:

  • pour Firefox, allez dans Édition->Préférences, icone 'Avancé', onglet 'Réseau', bouton 'Paramètres', champ 'Configuration manuelle du proxy';
  • pour lynx et wget, il faut de mettre la variable d'environnement http_proxy à http://localhost:8080;
  • pour links, on utilisera l'option -http-proxy localhost:8080 en ligne de commande.

  1. Dans un premier temps, vous programmerez uniquement la partie serveur du proxy HTTP.

    Votre proxy se contentera de répondre une erreur 403 Forbidden à toutes les requêtes du client, sans chercher à contacter de serveur WEB. Un tel programme commence par créer une socket d'écoute sur le port 8080 (socket, bind, listen). Ensuite, dans une boucle infinie, il attend une nouvelle connection (accept), traite une requête (<>, print) et ferme la connection (close).

    Alternativement à socket, bind, listen,accept, vous pouvez utiliser l'interface de plus haut niveau (orientée-objet) du module IO::Socket::INET.

  2. Ajoutez maintenant la partie client afin de réaliser un proxy HTTP transparent.

    Celui-ci retransmet chaque requête au serveur indiqué dans l'en-tête Host et renvoie la réponse au client. Testez votre proxy avec plusieurs clients HTTP (Firefox, Konqueror, Lynx, Links, wget, etc). Pensez à bien gérer le cas où le serveur demandé par le client n'est pas joignable (502 Bad Gateway).

    Pensez également à forcer le mode Connection: close afin que le serveur ferme la socket à la fin de la réponse.

Notez qu'un proxy réaliste devrait pouvoir traiter plusieurs connections en parallèle (par exemple, à l'aide de select, de fork ou des threads).

top

Exercice 2 - Gestion des connections keep-alive

Par défaut, si ni le client ni le serveur ne précise l'en-tête Connection: close (ou Proxy-connection: close pour une connection client vers proxy), le serveur (ou proxy) ne fermera pas la connection TCP après avoir répondu à la requête du client. Ceci permet au client de réutiliser la connection pour envoyer plusieurs requêtes, ce qui augmente l'efficacité du protocole. L'efficacité est encore augmentée si le client effectue un tir groupé (pipelining) de plusieurs requêtes avant de lire les réponses: cela permet au serveur de commencer à traiter la deuxième requête alors que la première réponse est en cours de transmission.

Il existe deux conventions pour indiquer au client (ou proxy) la fin d'une réponse à une requête:

  • si l'en-tête Content-Length est présente, elle précise la taille, en octets et en décimal, de la réponse (en-têtes non comprises),

  • si l'en-tête Transfer-Encoding: chunked est présente, la réponse est constituée d'une suite de blocs de taille variable; chaque bloc est précédé d'une ligne contenant sa taille en octets et en hexadécimal et est suivi d'une fin de ligne \r\n; la fin de la réponse est indiquée par une ligne indiquant une taille de 0, suivie de plusieurs lignes de textes puis d'une ligne vide.

Modifiez votre proxy pour qu'il gère les connections keep-alive.

top

Exercice 3 - Proxy filtrant

Modifiez le proxy initial pour qu'il ne télécharge pas les images. À la place, le proxy pourra envoyer au client la chaîne suivante, correspondant à une image PNG transparente de taille 1x1:

"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52
\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4
\x89\x00\x00\x00\x0D\x49\x44\x41\x54\x08\xD7\x63\x60\x60\x60\x60
\x00\x00\x00\x05\x00\x01\x5E\xF3\x2A\x3A\x00\x00\x00\x00\x49\x45
\x4E\x44\xAE\x42\x60\x82"

Pour reconnaître les images, on pourra se baser sur l'extension de l'URL ou bien examiner l'en-tête Content-type donnée par le serveur. De plus, on pourra filtrer les URLs de façon à ne bloquer que les images provenant de sites de publicité. Pour cela, on lira dans un fichier de configuration .ads une expression régulière indiquant les URLs à filtrer (e.g.: (.*adserver.*)|(/ad\..*)|(/ads\..*)).

top

Exercice 4 - Proxy cache

Ajoutez au proxy initial un cache qui garde en mémoire les pages WEB envoyées. Si la page est à nouveau demandée, le proxy renverra la copie qu'il a en cache au lieu de se connecter au serveur. Vous pourrez implanter le cache par une simple table de hachage en mémoire vive qui associe à une URL son contenu.

Vous prendrez soin à gérer les en-têtes Cache-control générées par le client ou le serveur. En particulier, un serveur qui ne souhaite pas que sa réponse soit stockée en cache précisera l'option no-cache, no-store ou must-revalidate (e.g.: la page est dynamique). Un client utilisera l'option max-age=0 ou no-cache pour forcer le proxy à redemander au serveur une nouvelle version de la page, même si une version antérieure se trouve en cache (e.g.: l'utilisateur clique sur l'icône actualiser).



Notes: Un cache plus complet tiendrait compte des en-têtes Expire, Date ainsi que des options max-age et min-fresh et max-stale de Cache-control pour déterminer si une entrée de cache peut être utilisée ou bien doit être rafraîchie. De plus, une implantation plus sophistiquée ferait intervenir un cache sur disque. Un exemple de proxy-cache HTTP couramment utilisé est le logiciel Squid.

top