Client HTTP/1.1

Le protocole HTTP

Ce texte présente un résumé très incomplet du protocole HTTP. Vous pourrez trouver plus d'informations sur la page Wikipedia ou dans les RFC du protocole HTTP : RFC 7230, RFC 7231, RFC 7232, RFC 7233, RFC 7234, RFC 7235, RFC 7236 et RFC 7237.

Requêtes et réponses

Dans le protocole HTTP, une fois établie une connexion TCP avec la machine hébergeant le serveur (par défaut, sur son port 80), le client envoie des requêtes au serveur, les plus courantes étant GET, HEAD et POST. Une requête GET demande au serveur de renvoyer une ressource (par exemple le code HTML d'une page web).
Le serveur répond par une réponse qui contiendra la ressource demandée.

Requête GET

Une requête GET en HTTP 1.1 est de la forme :

GET /~carayol/ HTTP/1.1
Host: igm.univ-mlv.fr

Chaque ligne est encodée en ASCII et se termine par deux caractères CR LF (Carriage Return, Line Feed; "\r\n" en java). Une requête commence par une ligne d'en-tête (avec le nom de la méthode de requête, ici GET). Elle peut contenir plusieurs lignes de champs d'en-tête (comme le champ Host ici) et se termine par une ligne vide.
La requête ci-dessus est envoyée sur la connexion TCP établie avec le port TCP 80 du serveur hébergeant le site igm.univ-mlv.fr, et elle demande la ressource /~carayol/ : comme il s'agit d'un répertoire, c'est en général le fichier index.html contenu dans ce répertoire qui sera renvoyé.

Réponse du serveur

La réponse du serveur est composée de deux parties :

La réponse du serveur est quelque chose du genre :

HTTP/1.1 200 OK
Date: Thu, 01 Mar 2018 17:28:07 GMT
Server: Apache
Last-Modified: Thu, 15 Sep 2016 09:02:49 GMT
ETag: "254441f-3d0a-53c881c25a040"
Accept-Ranges: bytes
Content-Length: 15626
Content-Type: text/html

<!DOCTYPE html>
<html>
<head>
...
Chaque ligne de l'en-tête est encodée en ASCII et est terminée par CRLF. La première ligne, dite ligne de statut :
HTTP/1.1 200 OK
donne la version du protocole utilisée pour la réponse (ici HTTP/1.1), le code de la réponse (200) et un message textuel correspondant à ce code (OK). Les lignes suivantes de l'en-tête sont de la forme :
clé: valeur
Parmi les informations pertinentes, Content-Length indique que le contenu (c'est à dire le "corps" de la réponse, qui débute après l'en-tête qui se termine par une line vide) fait 15626 octets. Cette information est nécessaire car en HTTP/1.1 le serveur ne ferme pas la connexion après avoir répondu. Le champ Content-Type indique que la ressource est du HTML.

Il existe un autre mode de transfert appelé chunked. Ce mode est signalé par le champ suivant :

Transfer-Encoding: chunked
Le principe est décrit ici. Pour l'instant, il n'est pas nécessaire de regarder ce mode de fonctionnement.

Client HTTP

Le but de cet exercice est de commencer la réalisation d'un petit client HTTP. Notre client permettra de faire des requêtes GET et d'afficher le corps de la réponse si c'est du HTML décodé dans le Charset spécifié dans l'en-tête. On utilisera la version 1.1 du protocole HTTP pour les requêtes.

La difficulté principale dans la réalisation d'un client HTTP est le traitement de la réponse du serveur. Le problème vient du fait qu'il n'y a pas, a priori, de borne sur la taille du header. Quand on fait une lecture sur la SocketChannel, on ne peut donc pas savoir, a priori, si on arrivera à lire une ligne d'en-tête, toutes les lignes d'en-tête, ou bien également une partie du corps de la requête...

Pour résoudre ces difficultés, nous vous proposons de réaliser le traitement de la réponse au moyen d'une classe HTTPReader que vous allez implémenter.

Récupérez les fichiers HTTPReader.java et HTTPException.java qui vous serviront de base.

Un objet de la classe HTTPReader contient deux champs : la SocketChannel sur laquelle la réponse est lue et son ByteBuffer de lecture.

Attention ! Toutes les méthodes de la classe doivent respecter le comportement suivant :
  • Le buffer de lecture est en mode écriture avant et après l'appel à la méthode (son contenu n'est pas "prêt" à être lu, mais prêt à être complété s'il reste de la place).
  • Aucune lecture sur la socket ne peut être effectuée tant que le buffer de lecture contient des éléments (on doit donc "consommer" tout le contenu du buffer avant de lire à nouveau des données).

Écrivez la méthode readLineCRLF dans la classe HTTPReader. Dans un premier temps, vous pouvez la tester avec l'exemple du main. Ensuite vous vérifierez que votre fonction passe les tests JUnit du fichier HTTPReaderTest.java (il vous faudra aussi le fichier FakeHTTPServer.java).

Écrivez la méthode readHeader. Vous devez renvoyer un objet de la classe HTTPHeader.java (qui vous est fournie).

Pour créer l'objet, vous utiliserez la factory create qui prend en paramètre :
  • une String contenant la première ligne de la réponse,
  • une map associant à chaque champ sa valeur.
    Attention, le header peut contenir plusieurs fois le même champ. Par exemple,
    Set-cookie: x-wl-uid=1smBggFQdYEUGLgg29x3Qr/zAwfq42jdGu0mYszL1+mrt/ABZ8xw43Ise90maJaHGuGvUKVQ+0gM=; path=/; domain=.amazon.fr; expires=Mon, 31-Dec-2035 23:00:01 GMT
    Set-cookie: session-id-time=2082754801l; path=/; domain=.amazon.fr; expires=Mon, 31-Dec-2035 23:00:01 GMT
    Set-cookie: session-id=276-2784413-9232431; path=/; domain=.amazon.fr; expires=Mon, 31-Dec-2035 23:00:01 GMT
    
    C'est équivalent à ne mettre qu'un seul champ Set-cookie avec la concaténation des 3 chaînes séparées par des point-virgules.

Compléter la méthode readBytes.

Écrivez un client HTTPClient qui prend en argument l'adresse d'un serveur et une ressource. Le client demande la ressource au serveur sur son port 80, il affiche la ressource si c'est du HTML.
Pour l'instant, vous ne traiterez que le cas où la réponse contient un champ Content-Length.

Faire en sorte de pouvoir traiter les réponses dans le mode de transfert chunked. Pour cela implémentez, la méthode readChunks dans la classe HTTPReader.

Modifiez votre client pour prendre en compte les codes de réponse 301 et 302.

Pour tester le code 302, vous pouvez demander la ressource /~carayol/redirect.php au site www-igm.univ-mlv.fr. Pour parser le champ location du header, vous pouvez utiliser la classe URI. Vous pouvez utiliser les méthodes d'instances getHost() pour obtenir l'adresse du serveur et getPath() pour obtenir le chemin de la ressource.