R. Fielding (Adobe)M. Nottingham (Fastly)J. Reschke (greenbytes)June20222022-06-07
Que voilà une épaisse lecture (252 pages). Mais c'est parce qu'il
s'agit de réécrire complètement la totalité des normes de HTTP. Pas le
protocole lui-même, je vous rassure, HTTP ne change pas. Mais la
rédaction de ses normes est profondément réorganisée, avec un
RFC (notre ) qui décrit une vision de haut niveau de HTTP, puis un
autre RFC par version majeure de HTTP, décrivant les détails de
syntaxe de chaque version.
Par exemple, HTTP/1 () a un
encodage en texte alors que HTTP/2 ()
a un encodage binaire. Pourtant, tous les deux suivent les mêmes
principes, décrits dans ce (méthodes comme
GET, en-têtes de la requête et de la réponse,
codes de retour à trois chiffres…) mais avec des encodages
différents, chacun dans son propre RFC. Notre est donc la vision de haut niveau de HTTP, commune à
toutes les versions, et d'autres RFC vous donneront les
détails. Inutile de dire que cette réorganisation a été un gros
travail, commencé en 2018.
Les trois versions de HTTP actuellement en large usage (1.1, 2 et
3) reposent toutes sur des concepts communs. Par exemple, les codes
d'erreur (comme le fameux 404) sont les
mêmes. Il n'est pas prévu, même à moyen terme, que les versions les
plus anciennes soient abandonnées (HTTP/1.1 reste d'un usage très
courant, et souvent pour de bonnes raisons). D'où cette
réorganisations des normes HTTP, avec notre qui
décrit ce qui est commun aux trois versions, d'autres RFC communs
aux trois versions, et un RFC par
version :
La mémorisation des données (),HTTP/1.1 (),HTTP/2 (),HTTP/3 ().
Vous connaissez certainement déjà HTTP, mais notre RFC ne présuppose
pas de connaissances préalables et explique tout en partant du
début, ce que je fais donc également ici. Donc, HTTP est un
protocole applicatif,
client/serveur, sans état, qui permet l'accès et la modification de
ressources distantes (une ressource pouvant être du texte, une
image, et étant générée dynamiquement ou pas, le protocole est
indépendant du format de la ressource ou de son mode de création, le
RFC insiste bien sur ce point). Le client se connecte, envoie une
requête, le serveur répond. HTTP ne fonctionne pas forcément de bout
en bout, il peut y avoir des relais sur le
trajet, et leur présence contribue beaucoup à certaines complexités
de la norme.
S'il fallait résumer HTTP rapidement, on pourrait dire qu'il
décrit un moyen d'interagir avec une
ressource distante (la ressource peut être un
fichier, un programme…). Il repose sur l'échange de messages, avec
une requête du client vers le serveur et une réponse en sens
inverse. Outre la méthode qui indique ce que le
client veut faire avec la ressource, HTTP permet de transporter des
métadonnées.
La section 3 du RFC décrit les concepts centraux de ce protocole,
comme celui de ressource présenté plus haut. (Qui est parfois appelé
« page » ou « fichier » mais ces termes ne sont pas assez
génériques. Une ressource n'est pas forcément une page HTML !) HTTP identifie
les ressources par des URI. Une représentation
est la forme concrète d'une ressource, les bits qu'on reçoit ou
envoie. (Du fait de la négociation de contenu et d'autres facteurs,
récupérer une ressource en utilisant le même URI ne donnera pas
forcément les mêmes bits, même s'ils sont censés être sémantiquement
équivalents.) La ressource n'est pas non plus forcément un fichier, pensez à une ressource qui indique l'heure qu'il est,
ou le temps qu'il fait, par exemple. Ou à l'URI
https://www.bortzmeyer.org/apps/random qui vous
renvoie une page choisie aléatoirement de ce blog. HTTP agit
sur une ressource (dont le type n'est pas forcément connu) via une
méthode qui va peut-être retourner une représentation de cette
ressource. C'est ce qu'on nomme le principe REST et de
nombreuses API se réclament de ce principe.
HTTP est un protocole client/serveur. Le
serveur attend le client. (Le client est parfois appelé
user agent.) Entre les deux, HTTP utilisera un
protocole de transport fiable, comme
TCP
(HTTP/1 et 2) ou QUIC (HTTP/3). Par
défaut, HTTP est sans état : une fois une requête servie, le serveur
oublie tout. Les clients sont très variés : il y a bien sûr les
navigateurs Web, mais
aussi les robots, des outils en ligne de
commande comme wget, des
objets connectés, des programmes vite faits en utilisant une des
zillions de bibliothèques qui permettent de développer rapidement un
client HTTP, des applications sur un
ordiphone, etc. Notamment, il n'y a pas
forcément un utilisateur humain derrière le client HTTP. (Pensez à
cela si vous mettez des éléments d'interfaces qui demandent qu'un
humain y réponde ; le client ne peut pas forcément faire de
l'interactivité.)
Les messages envoyés par le client au serveur sont des
requêtes et ceux envoyés par le serveur des
réponses.
La section 2 du RFC explique ce qu'on attend d'un client ou d'un
serveur HTTP conforme. Un point important et souvent ignoré est que
HTTP ne donne pas de limites quantitatives à beaucoup de ses
éléments. Par exemple, la longueur maximale de la première ligne de
la requête (celle qui contient le chemin de la ressource) n'est pas
spécifiée, car il serait trop difficile de définir une limite qui
convienne à tous les cas, HTTP étant utilisé dans des contextes très
différents. Comme les programmes ont forcément des limites, cela
veut dire qu'on ne peut pas toujours compter sur une limite bien
connue.
Une mise en œuvre conforme pour HTTP doit notamment bien gérer la
notion de version de HTTP. Cette version s'exprime par deux chiffres
séparés par un point, le premier chiffre étant la version majeure
(1, 2 ou 3) et le second la mineure (il est optionnel, valant 0 par
défaut, donc HTTP/2 veut dire la même chose que
HTTP/2.0). Normalement, au sein d'une même version majeure, on doit
pouvoir interopérer sans trop de problème alors qu'entre deux
versions majeures, il peut y avoir incompatibilité totale. La
sémantique est forcément la même (c'est du HTTP, après tout) mais la
syntaxe peut être radicalement différente (pensez à l'encodage texte
de HTTP/1 vs. le binaire de HTTP/2 et 3). Donc, être conforme à
HTTP/1.1 veut dire lire ce mais aussi
le , qui décrit la syntaxe spécifique
de HTTP/1.1.
Comme, dans la nature, des programmes ne sont pas corrects, le
RFC autorise du bout des lèvres à utiliser le contenu des champs
User-Agent: ou Server: de
l'en-tête pour s'ajuster à des bogues connues (mais, normalement, ce
doit être uniquement pour contourner des bogues, pas pour servir un
contenu différent).
De même qu'un client HTTP n'est pas forcément un navigateur Web,
un serveur HTTP n'est pas forcément une grosse machine dans un
centre de données chez un
GAFA. Le serveur HTTP peut parfaitement être
une imprimante, un petit objet connecté, une caméra de
vidéosurveillance, un Raspberry Pi dans son
coin… Le RFC parle de « serveur d'origine » pour le serveur qui va
faire autorité pour les données servies. Pourquoi ce concept ? Parce
que HTTP permet également l'insertion d'un certain nombre
d'intermédiaires, les relais (proxy ou
gateway en anglais), entre le client et le
serveur d'origine. Leurs buts sont très variés. Par exemple, un
relais (proxy, pour le RFC) dans le réseau local
où se trouve le client HTTP peut servir à mémoriser les ressources
Web les plus souvent demandées, pour améliorer les performances. Un
relais (gateway ou reverse
proxy, pour le RFC) qui est au contraire proche du serveur
d'origine peut servir à répartir la charge
entre diverses instances. Revenons à la mémorisation des ressources
(caching en anglais). La mémoire
(cache en anglais) est un stockage de ressources
Web déjà visitées, prêtes à être envoyées aux clients locaux pour
diminuer la latence. La mémorisation
est un sujet suffisamment fréquent et important pour avoir son
propre RFC, le .
On a vu que HTTP servait à agir sur des ressources distantes. Des
ressources, il y en a beaucoup. Comment les identifier ? Le Web va
utiliser des URI comme
identificateurs. Ces URI sont normalisés dans
le , mais qui ne spécifie qu'une
syntaxe générique. Chaque plan d'URI (la chaine
de caractères avant le deux-points, souvent
appelée à tort protocole) doit spécifier un certain nombre de
détails spécifique à ce plan. Pour les plans
http et https, cette
spécification est la section 4 de notre RFC. (Tous les plans sont
dans un
registre IANA.) Un URI de plan http ou
https indique forcément une autorité (un
identificateur du serveur d'origine, en pratique un nom de machine)
et un chemin (identificateur de la ressource à l'intérieur d'un
autorité. Ainsi, dans
https://www.afnic.fr/observatoire-ressources/consultations-publiques/,
le plan est https, l'autorité
www.afnic.fr et le chemin
/observatoire-ressources/consultations-publiques/. Le
port par défaut est 80 pour
http et 443 pour
https (tous les deux sont enregistrés
à l'IANA). La différence entre les deux plans est que
https implique l'utilisation du protocole de
sécurité TLS
(), pour assurer notamment la
confidentialité des requêtes.
En théorie, un URI de plan http et un autre
identique, sauf pour l'utilisation de https,
sont complètement distincts. Ils ne représentent pas la même origine
(l'origine est un triplet {plan, machine, port}) et les deux
ressources peuvent être complètement différentes. Mais notre RFC
note que certaines normes violent ce principe, notamment celle sur
les cookies (),
avec parfois des conséquences fâcheuses pour la sécurité.
En HTTPS, puisque ce protocole s'appuie sur TLS, le serveur
présente un certificat, que le client doit
vérifier (section 4.3.4), en suivant les règles du .
Pour expliquer plusieurs des propriétés de HTTP, je vais beaucoup
utiliser le logiciel curl, un
client HTTP en ligne de commande, dont
l'option -v permet d'afficher tout le dialogue
HTTP. Si vous voulez faire des essais vous aussi, interrompez
momentanément votre lecture pour installer curl. […] C'est fait ? On
peut reprendre ?
GET / HTTP/1.1
> Host: www.hambers.mairie53.fr
> User-Agent: curl/7.68.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 02 Mar 2022 16:25:02 GMT
< Server: Apache
...
< Content-Length: 61516
< Content-Type: text/html; charset=UTF-8
<
...
]]>
Nous avons vu que la requête et la réponse HTTP contenaient des
métadonnées dans un en-tête composé de champs. (Il peut aussi y
avoir un pied, une sorte de post-scriptum, mais c'est peu utilisé.)
Chaque champ a un nom et une valeur. La section 5 du RFC détaille
cet important concept. Les noms de champs sont
insensibles à la casse. Ils sont enregistrés
dans un
registre IANA spécifique à HTTP (ils étaient avant dans le
même registre que les champs du courrier
électronique). Un client, un serveur ou un relais HTTP
doivent ignorer les champs qu'ils ne connaissent pas, ce qui permet
d'introduire de nouveaux champs sans tout casser. Le même champ peut
apparaitre plusieurs fois. Comme pour d'autres éléments du protocole
HTTP, la norme ne fixe pas de limite de taille. La valeur d'un champ
peut donc être très grande.
La valeur d'un champ obéit à des règles qui dépendent du
champ. Les caractères doivent être de l'ASCII, une
limite très pénible de HTTP. Si on veut utiliser
Unicode (ou un autre jeu de caractères), il
faut l'encoder comme indiqué dans le . Le RFC rappelle qu'autrefois
Latin-1 était autorisé (avec l'encodage du
pour les autres jeux) mais cela ne
devrait normalement plus être le cas (mais ça se rencontre parfois
encore). Si une valeur comprend plusieurs termes, ils doivent
normalement être séparés par des virgules (et on met entre
guillemets les valeurs qui comprennent des
virgules). Les valeurs peuvent inclure des paramètres, écrits sous
la forme nom=valeur. Certaines valeurs ont leur propre structure
(). Ainsi, plusieurs champs peuvent
inclure une estampille temporelle. La syntaxe pour celles-ci n'est
hélas pas celle du mais celle de
l'IMF (), plus
complexe et plus ambigüe. (Sans compter, vu l'âge de HTTP, qu'on
rencontre parfois de vieux formats comme celui du .) Voici des exemples de champs vus avec curl :
GET / HTTP/1.1
> Host: confiance-numerique.clermont-universite.fr
> User-Agent: curl/7.68.0
> Accept: */*
< HTTP/1.1 200 OK
< Date: Fri, 28 Jan 2022 17:21:38 GMT
< Server: Apache/2.4.6 (CentOS)
< Last-Modified: Tue, 01 Sep 2020 13:29:41 GMT
< ETag: "1fbc1-5ae4082ec730d"
< Accept-Ranges: bytes
< Content-Length: 129985
< Content-Type: text/html; charset=UTF-8
<
Séminaire Confiance Numérique
]]>
Le client HTTP (curl) a envoyé trois champs,
Host: (le serveur qu'on veut contacter),
User-Agent: (une chaine de caractères décrivant
le client) et Accept: (les formats acceptés,
ici tous). Le serveur a répondu avec divers champs comme
Server: (l'équivalent du
User-Agent:) et
Content-Type: (le format utilisé, ici
HTML). Et voici ce qu'envoie le navigateur Firefox :
On notera surtout un Accept: plus complexe
(curl accepte tout car il ne s'occupe pas de l'affichage).
Maintenant, les messages (requêtes et réponses). La façon exacte
dont ils sont transmis dépend de la version de HTTP. Par exemple, la
version 1 les encode en texte alors que les versions 2 et 3
préfèrent le binaire. Autre exemple, la version 3 ne prévoit pas de
mécanisme de début et de fin d'un message car chaque ruisseau QUIC ne porte qu'un seul message, un peu comme
les versions 0 de HTTP, avec le ruisseau QUIC au lieu de la
connexion TCP (notez qu'avec TCP sans TLS, le client peut ne pas savoir s'il a
bien reçu toutes les données). La section 6 de notre RFC ne donne
donc qu'une description abstraite. Un message comprend donc une
information de contrôle (la première ligne, dans le cas de HTTP/1,
un « pseudo en-tête » avec des noms de champs commençant par un
deux-points pour les autres versions), un
en-tête, un corps (optionnel) et un pied (également
optionnel). L'information de contrôle donne plusieurs informations
nécessaires pour la suite, comme la version de HTTP utilisée. Le
contenu (le corps) est juste une suite d'octets, que HTTP transporte
sans l'interpréter (ce n'est pas forcément de l'HTML). Dans la réponse,
l'information de contrôle comprend notamment un code numérique de
trois chiffres, qui indique comment la requête a été traitée (ou
pas).
Beaucoup moins connu que l'en-tête, un message peut aussi
comporter un pied, également composé de champs « nom: valeur ». Il
est nécessaire de les utiliser dans les cas où l'information est
générée dynamiquement et que certaines choses ne peuvent être
déterminées qu'après coup (une signature
numérique, par exemple).
Dans le cas le plus simple, le client HTTP parle directement au
serveur d'origine et il n'y a pas de complications de routage du
message. Le serveur traite le message reçu, point. Mais HTTP permet
d'autres cas, par exemple avec un relais qui
reçoit le message avant de le transmettre au « vrai » serveur
(section 7 du RFC). Ainsi, dans une requête, l'information de
contrôle n'est pas forcément un simple chemin
(/publications/cahiers-soutenabilites) mais
peut être un URL complet
(https://www.strategie.gouv.fr/publications/cahiers-soutenabilites). C'est
ce que fait le client HTTP s'il est configuré pour utiliser un
relais, par exemple pour mémoriser les réponses des requêtes (), ou bien parce que l'accès direct aux
ports 80 et 443 est
bloqué et qu'on est obligé d'utiliser un relais. Dans le cas où la
ressource demandées est identifiée par un URL complet, le relais
doit alors se transformer en client HTTP et faire une requête vers
le serveur d'origine (ou bien vers un autre relais…).
La section 8 de notre RFC s'attaque à une notion cruciale en
HTTP, celle de représentation. La
représentation d'une ressource est la suite d'octets qu'on obtient
en réponse à une requête HTTP (« représentation » est donc plus
concret que « ressource »). Une même ressource peut avoir plusieurs
représentations, par exemple selon les métadonnées que le client a
indiqué dans sa requête. Le type de la représentation est indiqué par le champ
Content-Type: de l'en-tête (et aussi par
Content-Encoding:). Sa valeur est un
type MIME (). Voici par exemple le type de la page que vous êtes
en train de lire :
Content-Type: text/html; charset=UTF-8
(Notez que le paramètre charset est mal nommé,
c'est en fait un encodage, pas un
jeu de caractères. L'erreur vient du fait que
dans les vieilles normes comme ISO-8859-1, les
deux concepts étaient confondus.)
Normalement, du fait de ce Content-Type:, le
client HTTP n'a pas à deviner le type de la représentation, il se
fie à ce que le serveur raconte. Ceci dit, certains clients ont la
mauvaise idée de chercher à deviner le
type. Cette divination est toujours incertaine (plusieurs
types de données peuvent se ressembler) et ouvre même la possibilité
de failles de sécurité.
Un autre champ, Content-Language:, indique
la langue de la représentation récupérée. Sa
valeur est une étiquette de langue, au sens
du . Si le texte est multilingue, ce
champ peut prendre plusieurs valeurs. Le RFC illustre cela avec le
traité de Waitangi, qui est en
maori et en anglais :
Content-Language: mi, en
Attention, la seule présence de différentes langues ne signifie pas
qu'il faut mettre plusieurs étiquettes de langue. Un cours
d'introduction à l'arabe écrit en français,
pour un public francophone, sera :
Content-Language: fr
Les étiquettes de langue peuvent être plus complexes que
l'indication de la seule langue, mais il me semble que c'est
rarement utilisé sur le Web.
La taille de la représentation, elle, est exprimée avec
Content-Length:, un champ très pratique pour le
client HTTP qui sait ainsi combien d'octets il va devoir lire (avant
HTTP/1, c'était facile, on lisait jusqu'à la fin de la connexion
TCP ;
mais ça ne marche plus depuis qu'il y a des connexions persistentes
et, de toute façon, en l'absence de TLS, cela ne permettait pas de
détecter des coupures prématurées). Évidemment, le client doit
rester paranoïaque et supposer que l'information puisse être
fausse. curl (avec -v) avertit ainsi, si la
taille indiquée est trop faible :
* Excess found in a read: excess = 1, size = 12, maxdownload = 12, bytecount = 0
Si la taille indiquée est trop grande, curl attend pour essayer de
lire davantage sur le connexion qui reste ouverte. Autre raison
d'être paranoïaque, la taille indiquée peut être énorme, menant par
exemple un
client imprudent, qui allouerait la mémoire demandée à épuiser
celle-ci. Sans compter l'éventualité d'un débordement
d'entier si la taille ne peut pas être représentée dans
les entiers utilisés par le client HTTP.
Ensuite vient un autre point pas forcément très connu : les
validateurs. HTTP permet d'indiquer des pré-conditions à la
récupération d'une ressource, pour épargner le réseau. Un client
HTTP peut ainsi demander « donne-moi cette ressource, si elle n'a
pas changé ». Pour cela, HTTP repose sur ces validateurs, qui sont
des métadonnées qui accompagnent la requête (avec des champs qui
expriment la requête conditionnelle, comme
If-Modified-Since:, et qui sont détaillés en
section 13) et que le serveur vérifiera. Il existe deux sortes de
validateurs, les forts et les faibles. Les faibles sont faciles à
générer mais ne garantissent pas une comparaison réussie, les forts
sont plus difficiles à faire mais sont plus fiables. Par exemple, un
condensat du contenu est fort. Il changera
forcément (sauf malchance inouïe) dès qu'on changera un seul bit du
contenu. Si le contenu est géré par un VCS, celui-ci fournit également des
validateurs forts : l'identificateur de
commit. Au contraire, une
estampille temporelle est un validateur faible. Si sa résolution est
d'une seconde, deux modifications dans la même seconde ne seront pas
détectées et le serveur croira à tort que le contenu n'a pas
changé.
Pour connaitre la valeur actuelle d'un futur validateur, le
client HTTP dispose de champs comme
Last-Modified: (une estampille temporelle) ou
ETag: (Entity Tag,
l'étiquette de la ressource, une valeur opaque, qui peut s'utiliser
avec des requêtes conditionnelles comme
If-None-Match:). Voici un exemple :
Last-Modified: Mon, 07 Feb 2022 12:20:20 GMT
ETag: "5278-5d76c9fc1c9f4"
(Le serveur utilisé était un Apache. Par
défaut, Apache génère des étiquettes qui sont un
condensat de divers attributs du fichier
comme l'inœud, la taille et la date de
modification. Apache permet de configurer
cet algorithme. Rappelez-vous que l'étiquette est opaque, le
serveur peut donc la générer comme il veut, il doit juste s'assurer
qu'elle change à chaque modification de la ressource. Le serveur peut par
exemple utiliser un SHA-1 du contenu de la ressource.)
A priori, l'étiquette de la ressource est un validateur fort,
autrement, le serveur doit la préfixer par W/
(W pour Weak).
Passons maintenant aux méthodes (section 9
du RFC). Il y a très longtemps, HTTP n'avait qu'une seule méthode
pour agir sur les ressources, la méthode
GET. Désormais, il y a nettement plus de
méthodes, chacune agissant sur la ressource indiquée d'une manière
différente et ayant donc une sémantique différente. Par exemple,
GET va récupérer une représentation de la
ressource, alors que PUT va au contraire écrire
le contenu envoyé, remplaçant celui de la ressource et que
DELETE va… détruire la ressource. La liste
complète des méthodes figure dans un
registre IANA.
Certaines des méthodes sont dites sûres car elles ne modifient
pas la ressource et ne casseront donc rien. Bien sûr, une méthode
sûre peut avoir des effets de bord (comme d'écrire une ligne dans le
journal du serveur, mais ce n'est pas la
faute du client). GET, HEAD
et les moins connues OPTIONS et
TRACE sont sûres. Du fait de cette garantie de
sûreté, un programme qui ne fait que des requêtes sûres a moins
d'inquiétudes à avoir, notamment s'il agit sur la base
d'informations qu'il ne contrôle pas. Ainsi, le
ramasseur d'un moteur de
recherche ne fait a priori que des requêtes sûres, pour
éviter qu'une page Web malveillante ne l'entraine à effectuer des
opérations qui peuvent changer le contenu des sites Web visités.
Une autre propriété importante d'une méthode est d'être
idempotente ou pas. Une méthode idempotente a
le même effet qu'on l'exécute une ou N fois. Les méthodes sûres sont
toutes idempotentes mais l'inverse n'est pas vrai :
PUT et DELETE sont
idempotentes (qu'on détruise une ressource une ou N fois donnera le
même résultat : la ressource est supprimée) mais pas
sûres. L'intérêt de cette propriété d'idempotence est qu'elles
peuvent être répétées sans risque, par exemple si le réseau a eu un
problème et qu'on n'est pas certain que la requête ait été
exécutée. Les méthodes non-idempotentes ne doivent pas, par contre,
être répétées aveuglément.
La méthode la plus connue et sans doute la plus utilisée,
GET, permet de récupérer une représentation
d'une ressource. La syntaxe avec laquelle s'exprime le chemin de
cette ressource fait penser à l'arborescence d'un système
de fichiers et c'est en effet souvent ainsi que c'est
mis en œuvre dans les serveurs (par exemple dans Apache, où le chemin, mettons
/foo/bar, est ajouté à la fin de la variable de
configuration DocumentRoot, avant d'être
récupéré sur le système de fichiers : si
DocumentRoot vaut
/var/www, le fichier demandé sera
/var/www/foo/bar). Mais ce n'est pas une
obligation de HTTP, qui ne normalise que le protocole entre le client
et le serveur, pas la façon dont le serveur obtient les
ressources.
La méthode HEAD fait la même chose que
GET mais sans renvoyer la représentation de la
ressource.
POST est plus compliquée. Contrairement à
GET, la requête contient des données qui vont
être envoyés au serveur. Celui-ci va les
traiter. POST est souvent utilisé pour
soumettre le contenu d'un formulaire Web, par
exemple pour envoyer un texte qui sera le contenu d'un commentaire
lors d'une discussion sur un forum Web. Avec
GET, POST est probablement
la méthode la plus souvent vue sur le Web.
PUT, lui, est également accompagné de
données qui vont être écrites à la place de la ressource
désignée. On peut donc mettre en œuvre un serveur de fichiers
distant avec des PUT et des
GET. On peut y ajouter
DELETE pour supprimer les ressources devenues
inutiles.
La méthode CONNECT est plus complexe. Elle
n'agit pas sur une ressource mais permet d'établir une connexion
avec un service distant. Sa principale utilité est de permettre
d'établir un tunnel au-dessus de
HTTP. Ainsi :
CONNECT server.example.com:80 HTTP/1.1
Host: server.example.com
va établir une connexion avec
server.example.com et les octets envoyés par la
suite sur cette connexion HTTP seront relayés aveuglément vers
server.example.com.
Quant à la méthode OPTIONS, elle permet
d'obtenir des informations sur les options gérées par le
serveur. curl permet d'indiquer une méthode avec son option
--request (ou -X) :
OPTIONS / HTTP/2
> Host: www.bortzmeyer.org
> user-agent: curl/7.68.0
> accept: */*
>
...
< HTTP/2 200
< permissions-policy: interest-cohort=()
< allow: POST,OPTIONS,HEAD,GET
...
]]>
La section 10 du RFC est ensuite une longue section qui décrit le
contexte des messages HTTP, c'est-à-dire les
métadonnées qui accompagnent requêtes et
réponses. Je ne vais évidemment pas en reprendre toute
la liste ici. Juste quelques exemples de champs
intéressants :
From: permet d'indiquer l'adresse de
courrier du
responsable du logiciel. Il est surtout utilisé par les
bots (par exemple ceux qui ramassent les pages pour le
compte d'un moteur de recherche) pour
indiquer qui contacter si le bot se comporte mal, par exemple en
faisant trop de requêtes. Comme le rappelle le RFC, un navigateur
ordinaire ne doit évidemment pas transmettre une telle donnée
personnelle à tous les sites Web visités !Referer: (oui, avec une faute d'orthographe) sert à indiquer
l'URL d'où vient le client HTTP. Le Web étant fondé sur l'idée
d'hypertexte, l'utilisateur est peut-être
venu ici en suivant un lien, et il peut ainsi indiquer où il a
trouvé ce lien, ce qui peut permettre au webmestre de voir d'où
viennent ses visiteurs. Lui aussi pose des problèmes de vie privée, et j'ai toujours été
surpris que le Tor Browser l'envoie.User-Agent: indique le type du client
HTTP. À part s'amuser en regardant le genre de visiteurs qu'on a,
il n'a pas de vraie utilité, le Web reposant sur des normes, et
précisant une structure et pas une présentation, il ne
devrait pas y avoir besoin de changer une ressource en fonction du
logiciel du visiteur. Mais c'est quand même ce que font certains
serveurs HTTP, poussant les clients à mentir pour obtenir un
certain résultat, ce qui donne des champs
User-Agent: ridicules comme (vu sur ce blog)
Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75
Safari/537.36 (probablement le navigateur
Safari indiquant autant de logiciels que possible ; le RFC dit qu'il ne faut pas le
faire mais c'est courant, pour tenir compte de serveurs qui
interprètent ce champ). Là encore, on a une métadonnée
qui contribue puissamment à la fuite d'information si commune sur
le Web (votre client HTTP est certainement trop bavard). Le
User-Agent: est très utile pour le
fingerprinting, l'identification d'un visiteur
particulier, comme le démontre le Panopticlick.Server: est l'équivalent de
User-Agent: mais pour le serveur.
Jusqu'à présent, on a supposé que les ressources servies étaient
accessibles à tous et toutes. Mais en pratique, on souhaite parfois
servir du contenu à accès restreint et on veut donc n'autoriser que certains visiteurs. Il
faut donc disposer de mécanismes
d'authentification, exposés dans la section
11 du RFC. HTTP n'a pas un mécanisme unique
d'authentification. Chaque mécanisme est identifié par un nom (et
les possibilités sont dans un
registre IANA). Le serveur indique le mécanisme à utiliser
dans un champ WWW-Authenticate: de sa première
réponse. Par exemple, basic, normalisé dans le
, est un mécanisme simple de
mot de passe, alors que
digest (normalisé dans le ) permet de s'authentifier via un
défi/réponse. Le mécanisme est spécifique à
un royaume, une information donnée par le serveur pour le cas où le
même serveur gérerait des types d'authentification différents selon
la ressource.
Voici un exemple d'authentification (avec le service
d'administration d'un serveur
dnsdist, celui utilisé pour mon résolveur
public) où l'identificateur est admin et
le mot de passe 2e12 :
GET / HTTP/1.1
> Host: doh.bortzmeyer.fr:8080
> Authorization: Basic YWRtbW...OTcyM=
> User-Agent: curl/7.68.0
> Accept: */*
>
...
< HTTP/1.1 200 OK
...
]]>
La représentation renvoyée peut dépendre du client, c'est ce
qu'on nomme la négociation de contenu (section 12 du RFC). La
méthode officielle est que le client annonce avec le champ
Accept: les types
de données qu'il accepte, et le serveur lui envoie de
préférence ce qu'il a demandé. (C'est utilisé sur ce blog pour les
images. En pratique, ça ne se passe pas toujours bien.)
La demande du client n'est pas strictement binaire « je veux du
format WebP ». Elle peut s'exprimer de
manière plus nuancée, via le système de qualité. Ainsi :
Accept: text/plain; q=0.5, text/html
signifie que le client comprend le texte brut
et l'HTML mais préfère ce dernier (le poids
par défaut est 1, supérieur, donc, au 0,5 du texte brut).
La négociation de contenu ne s'applique pas qu'au format des
représentations, elle peut aussi s'appliquer à la
langue, avec le champ
Accept-Language:. Ainsi, en disant :
Accept-Language: da, en;q=0.8
veut dire « je préfère le danois (poids de 1
par défaut), mais j'accepte l'anglais ». En
pratique, ce n'est pas très utile sur le Web car cela ne permet pas
d'indiquer la qualité de la traduction. Si on indique qu'on préfère
le français, mais qu'on peut lire l'anglais, en visitant des sites
Web d'organisations internationales, on se retrouve avec un texte
français mal traduit, alors qu'on aurait préféré la version originale. En outre,
comme beaucoup de champs de l'en-tête de la requête, il contribue à
identifier le client (fingerprinting). C'est
d'autant plus gênant que l'indication des langues préférées peut
vous signaler à l'attention de gens peu sympathiques, si ces langues
sont celles d'une minorité opprimée.
Comme la représentation envoyée peut dépendre de ces demandes du
client, le serveur doit indiquer dans sa réponse s'il a effectivement
tenu compte de la négociation de contenu. C'est notamment important
pour les relais Web qui mémorisent le contenu des réponses (). Le champ Vary:
permet d'indiquer de quoi a réellement dépendu la réponse. Ainsi :
Vary: accept-language
indique que la réponse ne dépendait que de la langue. Si un client
d'un relais Web demande la même ressource
mais avec une autre langue, il ne faut pas lui donner le contenu mémorisé.
HTTP permet d'exprimer des requêtes conditionnelles. Un client
HTTP peut par exemple demander une ressource « sauf si elle n'a pas
été modifiée depuis 08:00 ». Cela permet d'économiser des
ressources, notamment dans les relais qui mémorisent . C'est également utile aux clients de
syndication, qui récupèrent régulièrement
(polling) le flux
Atom pour voir s'il a changé. Les requêtes
conditionnelles permettent, la plupart du temps, d'éviter tout
téléchargement de ce flux.
Un autre scénario d'utilisation des requêtes conditionnelles est
le cas de la mise à jour perdue (lost
update). Prenons un client qui met à jour une ressource,
en récupérant d'abord son état actuel (avec un
GET), en la modifiant, puis en téléversant la
version modifiée (avec par exemple un PUT). Si
deux clients font l'opération à peu près en même temps, il y a un
risque d'une séquence :
Client 1 fait un GET,Client 2 fait pareil,Client 1 fait un PUT avec sa version
modifiée,Client 2 en fait autant,La mise à jour de Client 1 est perdue
Les mises à jour conditionnelles résoudraient ce problème : si
Client 2 fait sa mise à jour en ajoutant « seulement si la ressource
n'a pas changé », son PUT sera refusé, il devra
refaire un GET et il récupérera alors les
changeements de Client 1 avant d'appliquer les siens.
En pratique, les requêtes conditionnelles se font avec des champs
comme If-Modified-Since: dont la valeur est une
date. Par exemple, le client de syndication qui a récupéré une page
le 24 février à 18:11, va envoyer un If-Modified-Since:
Thu, 24 Feb 2022 18:11:00 GMT et le serveur ne lui
enverra la ressource Atom ou
RSS que si elle est plus récente (il recevra
un code de retour 304 dans le cas contraire). Autre exemple de
champ d'en-tête pour les requêtes conditionnelles,
If-Match:. Ce champ demande que la requête ne
soit exécutée que si la ressource correspond à la valeur du
If-Match:. La valeur est un validateur, comme
expliqué plus haut. If-Match: permet ainsi de
résoudre le problème de la mise à jour perdue.
curl -v vous affichera entre autres ce code
de retour. Si vous ne voulez que le code, et pas tous les messages
que l'utilisation de -v entrainera, l'option
--write-out est bien pratique :
% curl --silent --write-out "%{http_code}\n" --output /dev/null https://www.bortzmeyer.org/1.html
200
% curl --silent --write-out "%{http_code}\n" --output /dev/null https://www.bortzmeyer.org/2.html
404
Sinon, pour rire avec les codes de statut HTTP, il existe des photos de chats et une
proposition d'illustrer ces codes par
des émojis.
Beaucoup de choses dans HTTP peuvent être étendues (section
16). On peut créer de nouvelles méthodes, de nouveaux codes de
retour, etc. Un logiciel client ou serveur ne doit donc pas
s'étonner de voir apparaitre des questions ou des réponses qui
n'existaient pas quand il a été programmé.
Ainsi, des nouvelles méthodes, en sus des traditionnelles
GET, POST,
PUT, etc , peuvent être créées, comme l'avait
été PATCH par le . La liste à jour des méthodes est dans un
registre IANA. Si vous programmez côté serveur, et que vous utilisez l'interface
CGI, la méthode est indiquée dans la variable
REQUEST_METHOD et vous pouvez la tester, par
exemple ici en Python :
if environ["REQUEST_METHOD"] == "FOOBAR":
... Do something useful
else:
return unsupported(start_response, environ["REQUEST_METHOD"])
Des codes de retour peuvent également être ajoutés, comme le 451
(censure) du . Là aussi, la liste
faisant autorité est le
registre IANA.
Bien sûr, le registre le plus dynamique, celui qui voit le plus
d'ajouts, est celui des champs de
l'en-tête. Mais attention : du fait qu'il bouge beaucoup, les
nouveaux champs ne seront pas compris et utilisés par une bonne
partie des logiciels. (Au passage, notre RFC rappelle que l'ancienne
convention de préfixer les noms de champs non officiels par un
X- a été abandonnée, par le .)
Et enfin, on peut étendre les listes de mécanismes d'authentification, et plusieurs autres.
Très utilisé, HTTP a évidemment connu sa part de problèmes de
sécurité. La section 17 du RFC analyse les principaux
risques. (Certains risques spécifiques sont traités dans d'autres
RFC. Ainsi, les problèmes posés par l'analyse de l'encodage textuel de HTTP/1 sont
étudiés dans le . Ceux liés aux
URL sont dans
le .) D'autre part, beaucoup de
problème de sécurité du Web viennent :
du code qui tourne sur le serveur ; notre RFC ne va pas
parler de la sécurité de WordPress,des failles du client (par exemple si en exécutant du code
JavaScript, il permet un accès excessif à la
machine cliente),de l'Internet lui-même (comme la divulgation de l'adresse IP source).
Cette section 17 se concentre sur les problèmes de sécurité de HTTP,
ce qui est déjà pas mal. Le RFC recommande la lecture des documents
OWASP pour le reste.
Bon, premier problème, la notion d'autorité. Une réponse fait
autorité si elle vient de l'origine, telle
qu'indiquée dans l'URL. Le client HTTP va donc dépendre du mécanisme
de résolution de nom. Si, par exemple, la machine du client utilise
un résolveur DNS menteur, tout est
fichu. On croit aller sur http://pornhub.com/
et on se retrouve sur une page Web de
l'ARCOM. Il est donc crucial que cette
résolution de noms soit sécurisée, par exemple en utilisant un
résolveur DNS de confiance, et qui valide les réponses avec
DNSSEC. HTTPS protège partiellement. Une des
raisons pour lesquelles sa protection n'est pas parfaite est qu'il
est compliqué de valider proprement (cf. ). Et puis les problèmes sont souvent non techniques,
par exemple la plupart des tentatives
d'hameçonnage ne vont pas viser l'autorité
mais la perception que l'utilisateur en a. Une page Web copiée sur
celle d'une banque peut être prise pour celle de la banque même si,
techniquement, il n'y a eu aucune subversion des techniques de
sécurité. Le RFC recommande qu'au minimum, les navigateurs Web
permettent d'examiner facilement l'URL vers lequel va un lien, et de
l'analyser (beaucoup d'utilisateurs vont croire, en voyant
https://nimportequoi.example/banque-de-confiance.com
que le nom de domaine est
banque-de-confiance.com…).
On a vu qu'HTTP n'est pas forcément de bout en bout et qu'il est
même fréquent que des intermédiaires se trouvent sur le
trajet. Évidemment, un tel intermédaire est idéalement placé pour
certaines attaques. Bref, il ne faut utiliser que des
intermédiaires de confiance et bien gérés. (De nombreuses
organisations placent sur le trajet de leurs requêtes HTTP des
boites noires au logiciel privateur qui espionnent le trafic et font
Dieu sait quoi avec les données récoltées.)
HTTP est juste un protocole entre le client et le serveur. Le
client demande une ressource, le serveur lui envoie. D'où le serveur
a-t-il tiré cette ressource ? Ce n'est pas l'affaire de HTTP. En
pratique, il est fréquent que le serveur ait simplement lu un
fichier pré-existant sur ses disques et, en outre, que le chemin
menant à ce fichier vienne d'une simple transformation de l'URL. Par
exemple, Apache,
avec la directive DocumentRoot valant
/var/doc/mon-beau-site et une requête HTTP
GET /toto/tata.html va chercher un fichier
/var/www/mon-beau-site/toto/tata.html. Dans ce
cas, attention, certaines manipulations sur le chemin donné en
paramètre à GET peuvent donner au client
davantage d'accès que ce qui était voulu. Ainsi, sans précautions
particulières, une requête GET
/../toto/tata.html serait traduite en
/var/www/mon-beau-site/../toto/tata.html, ce
qui, sur Unix, équivaudra à
/var/www/toto/tata.html, où il n'était
peut-être pas prévu que le client puisse se promener. Les auteurs de
serveurs doivent donc être vigilants : ce qui vient du client n'est
pas digne de confiance.
Autre risque lorsqu'on fait une confiance aveugle aux données
envoyées par le client, l'injection. Ces données, par exemple le
chemin dans l'URL, sont traitées par des langages qui ont des règles
spéciales pour certains caractères. Si un de ces caractères se
retrouve dans l'URL, et qe le programme, côté serveur, n'est pas
prudent avec les données extérieures, le ou les caractères spéciaux
seront interprétés, avec parfois d'intéressantes failles de sécurité
à la clé. (Mais, attention, tester la présence de « caractères dangereux » n'est en
général pas une bonne idée.)
La liste des questions de sécurité liées à HTTP ne s'arrête pas
là. On a vu que HTTP ne mettait pas de limite de taille à des
éléments comme l'URL. Un analyseur imprudent, côté serveur, peut se
faire attaquer par un client qui enverrait un chemin d'URL très long,
déclenchant par exemple un débordement de
tableau.
HTTP est un protocole très bavard, et un client HTTP possède
beaucoup d'informations sur l'utilisateur humain qui est
derrière. Le client doit donc faire très attention à ne pas envoyer
ces données. Le RFC ne donne pas d'exemple précis mais on peut par
exemple penser au champ Referer qui indique
l'URL d'où on vient. Si le client l'envoie systématiquement, et que
l'utilisateur visitait un site Web interne de l'organisation avant de
cliquer vers un lien externe, son navigateur enverra des détails sur
le site Web interne. Autre cas important,
un champ comme Accept-Language, qu'on peut
estimer utile dans certains cas, est dangereux pour la vie privée,
transmettant une information qui peut être sensible, par exemple si
on a indiqué une langue minoritaire et mal vue dans son pays. Et
User-Agent facilite le ciblage d'éventuelles
attaques du serveur contre le client.
Du fait de ce caractère bavard, et aussi parce que, sur
l'Internet, il y a des choses qu'on ne peut pas dissimuler
facilement (comme l'adresse
IP source), ce que le serveur stocke dans ses
journaux est donc sensible du point de vue de
la vie privée. Des lois
comme la loi Informatique & Libertés
encadrent la gestion de telles bases de données personnelles. Le contenu
de ces journaux doit donc être protégé contre les accès
illégitimes.
Comme HTTP est bavard et que le client envoie beaucoup de choses
(comme les Accept-Language et
User-Agent cités plus haut), le serveur peut
relativement facilement faire du
fingerprinting,
c'est-à-dire reconnaitre un client HTTP parmi des dizaines ou des
centaines de milliers d'aures. (Vous ne me croyez pas ? Regardez le
Panopticlick.) Un serveur peut ainsi suivre un client
à la trace, même sans
cookies (voir
« A
Survey on Web Tracking: Mechanisms, Implications, and
Defenses »).
Voilà, et encore je n'ai présenté ici qu'une partie des questions
de sécurité liées à l'utilisation de HTTP. Lisez le RFC pour en
savoir plus. Passons maintenant aux différents registres IANA qui
servent à stocker les différents éléments du protocole HTTP. Je les
ai présenté (au moins une partie d'entre eux !) plus haut mais je
n'ai pas parlé de la politique d'enregistrement de nouvaux
éléments. En suivant la terminologie du , il y a entre autres le registre des méthodes (pour ajouter une nouvelle
méthode, il faut suivre la politique « Examen par l'IETF », une des
plus lourdes), le registre des codes de
retour (même politique), le registre
des champs (désormais séparé de celui des champs du
courrier,
politique « Spécification nécessaire »), etc.
Ah, et si vous voulez la syntaxe complète de HTTP sous forme
d'une grammaire formelle, lisez
l'ABNF en annexe A.
Et avec un langage de programmation ? Vu le succès de HTTP et sa
présence partout, il n'est pas étonnant que tous les langages de
programmation permettent facilement de faire des requêtes
HTTP. HTTP, en tout cas HTTP/1, est suffisamment simple pour qu'on
puisse le programmer soi-même en appelant les fonctions réseau de
bas niveau, mais pourquoi s'embêter ? Utilisons les bibliothèques
prévues à cet effet et commençons par le langage
Python. D'abord avec la bibliothèque
standard http.client :
conn = http.client.HTTPConnection(HOST)
conn.request("GET", PATH)
result = conn.getresponse()
body = result.read().decode()
Et hop, la variable body contient une
représentation de la ressource demandée (le programme complet est en
). En pratique, la plupart des
programmeurs Python utiliseront sans doute une autre
bibliothèque standard, qui n'est pas spécifique à HTTP et
permet de traiter des URL quelconques (cela
donne le programme ). D'encore
plus haut niveau (mais pas incluse dans la bibliothèque standard, ce
qui ajoute une dépendance à votre programme) est la bibliothèque
Requests, souvent utilisée (voir par exemple ).
Ensuite, avec le langage Go. Là aussi, il
dispose de HTTP dans sa
bibliothèque standard :
response, err := http.Get(Url)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
Le programme complet est .
Et ici un client en Elixir, utilisant la
bibliothèque HTTPoison :
HTTPoison.start()
{:ok, result} = HTTPoison.get(@url)
La version longue est en .
L'annexe B de notre RFC fait la liste des principaux changements
depuis les précédents RFC. Je l'ai dit, le protocole ne change pas
réellement mais il y a quand même quelques modifications, notamment
des clarifications de textes trop ambigus (par exemple la définition
des intervalles). Et bien sûr le gros changement est qu'il y a
désormais une définition abstraite de ce qu'est un message HTTP,
séparée des définitions concrètes pour les trois versions de HTTP en
service. En outre, il y a désormais des recommandations explicites de taille
minimale à accepter pour certains élements (par exemple 8 000 octets
pour les URI).
HTTP est, comme vous le savez, un immense succès, dû à la place
prise par le Web, dont il est le protocole de
référence. Le RFC résume l'histoire de HTTP :
Publié pour la première fois en
1990, au début un protocole ultra-simple,
réduit à la méthode GET, et qui n'a été
documenté qu'après,HTTP/1.0 ( en 1996 mais le protocole
existait bien avant le RFC) en a fait un protocole bien plus riche (en-tête,
plusieurs méthodes), avec
indication du type des
ressources, et virtual
hosting,HTTP/1.1 en 1995 (normalisé dans le à
l'origine, et aujourd'hui ), au moment
de l'explosion de l'usage du Web, a apporté beaucoup d'améliorations, comme
le fait que les connexions sont persistentes par défaut,HTTP/2 a marqué plusieurs ruptures en 2015 (), avec son parallélisme (les réponses n'arrivent
plus forcément dans l'ordre des requêtes) et son encodage binaire,
rompant avec la tradition textuelle de beaucoup de protocoles
IETF,Et enfin HTTP/3 (, en 2022) a
abandonné TCP pour QUIC,