Première rédaction de cet article le 9 juillet 2007
Dernière mise à jour le 12 juillet 2007
Un des aspects les moins connus du protocole HTTP est la possibilité de négociation de contenus par laquelle le client HTTP informe le serveur de ses préférences, en matière de format de données, ou bien de langue.
Cela permet de changer le format des données envoyées en fonction des capacités du client. Par exemple, si un navigateur comprend les formats d'image GIF et JPEG, il peut l'indiquer au serveur Web, qui lui enverra alors ces formats, s'il en dispose. Ainsi, il n'est plus nécessaire d'indiquer un format particulier, ce qui nécessite de se demander si tous les navigateurs l'acceptent.
En effet, sans la négociation de contenu, déployer un nouveau format d'images comme SVG serait périlleux, tous les navigateurs ne l'acceptant pas. Avec la négociation de contenu, on laisse les navigateurs qui sont capables de lire le SVG le demander explicitement.
Dans les pages HTML, on peut alors écrire :
<img src="/mon-beau-dessin"/>
sans mettre d'extension (comme .png
)
après le nom de fichier (autre avantage, les
URL sont plus propres). Côté serveur, pour Apache, il faut activer l'option
MultiViews par exemple en mettant Options
+Multiviews
dans la configuration d'Apache ou bien dans un
.htaccess
.
Cette négociation de contenu (mal nommée car il n'y a pas de
vraie négociation : le client donne sa liste et le serveur répond) se
fait avec l'en-tête Accept
du protocole
HTTP, en-tête décrit dans la section 5.3.2 du RFC 7231. Cet en-tête prend comme paramètre un couple
type/sous-type
où le type et le sous-type sont
décrits par MIME. Par exemple, une image
PNG sera image/png
, un
texte brut sera text/plain
, etc.
Voici le dialogue entre le client
curl et un serveur Apache. On a utilisé l'option
--header
de curl pour indiquer les choix (par
défaut, curl indique qu'il accepte */*
c'est-à-dire tout).
% curl -v --header 'Accept: image/png, */*' http://www.bortzmeyer.org/images/attaque-dns-via-orn ... > GET /images/attaque-dns-via-orn HTTP/1.1 User-Agent: curl/7.13.2 (i386-pc-linux-gnu) libcurl/7.13.2 OpenSSL/0.9.7e zlib/1.2.2 libidn/0.5.13 Accept: image/png, */* ... < HTTP/1.1 200 OK < Content-Location: attaque-dns-via-orn.png < Vary: negotiate,accept < Content-Length: 96405 < Content-Type: image/png ...
On voit que le serveur a indiqué le nom choisi avec l'en-tête
Content-Location
et le format, comme d'habitude,
avec Content-Type
. Par contre, dans le journal
d'Apache, l'information n'apparait pas explicitement. Il faut utiliser
la taille du fichier (ici 96405 octets) pour savoir quelle version a été servie par
Apache :
192.134.4.69 - - [09/Jul/2007:17:12:51 +0200] "GET /images/attaque-dns-via-orn HTTP/1.1" 200 96405 "-" "curl/7.15.5 (i486-pc-linux-gnu) libcurl/7.15.5 OpenSSL/0.9.8c zlib/1.2.3 libidn/0.6.5" www.bortzmeyer.org
L'en-tête Vary
dans la réponse permet aux
éventuels caches Web sur le trajet de savoir
que la ressource n'est pas identifiée uniquement par un URL mais par
une combinaison de l'URL et du Accept
.
Si je demande un format que le serveur n'a pas et que je ne spécifie pas d'alternative (*/*
) :
% curl -v --header 'Accept: image/toto' --output /dev/null http://www.bortzmeyer.org/images/attaque-dns-via-orn > GET /images/attaque-dns-via-orn HTTP/1.1 User-Agent: curl/7.13.2 (i386-pc-linux-gnu) libcurl/7.13.2 OpenSSL/0.9.7e zlib/1.2.2 libidn/0.5.13 Accept: image/toto ... < HTTP/1.1 406 Not Acceptable < Alternates: {"attaque-dns-via-orn.gif" 1 {type image/gif} {length 16509}}, {"attaque-dns-via-orn.jpg" 1 {type image/jpeg} {length 87370}}, {"attaque-dns-via-orn.png" 1 {type image/png} {length 96405}}, {"attaque-dns-via-orn.svg" 1 {type image/svg+xml} {length 19603}}
Le serveur répond par le code 406 (section 10.4.7 du RFC), qui signifie qu'il n'a pas ce
type et l'en-tête Alternates
lui sert à indiquer
de quels formats il dispose. Et merci à Kim Minh Kaplan pour avoir détecté la bogue qu'il y avait dans cet exemple.
À travers un cache Web (ici, Squid), on voit
l'utilité de l'en-tête Vary
:
% curl --header 'Accept: image/gif, */*' -v http://preston.sources.org/tmp/toto.image > GET http://preston.sources.org/tmp/toto.image HTTP/1.1 User-Agent: curl/7.13.2 (i386-pc-linux-gnu) libcurl/7.13.2 OpenSSL/0.9.7e zlib/1.2.2 libidn/0.5.13 Accept: image/gif, */* ... < HTTP/1.0 200 OK < Content-Location: toto.gif < Vary: negotiate,accept < Content-Length: 6384 < Content-Type: image/gif < X-Cache-Lookup: HIT from cache.sources.org:3128
Si un second client passe après en préférant des images JPEG, il les obtiendra :
% curl --header 'Accept: image/jpeg, */*' -v http://preston.sources.org/tmp/toto.image > GET http://preston.sources.org/tmp/toto.image HTTP/1.1 User-Agent: curl/7.13.2 (i386-pc-linux-gnu) libcurl/7.13.2 OpenSSL/0.9.7e zlib/1.2.2 libidn/0.5.13 Accept: image/jpeg, */* ... < HTTP/1.0 200 OK < Content-Location: toto.jpg < Vary: negotiate,accept < Content-Length: 3661 < Content-Type: image/jpeg < X-Cache-Lookup: HIT from cache.sources.org:3128
Et si le client HTTP ne spécifie rien de particulier, question type ?
% curl -v http://www.bortzmeyer.org/images/attaque-dns-via-orn > GET /images/attaque-dns-via-orn HTTP/1.1 User-Agent: curl/7.13.2 (i386-pc-linux-gnu) libcurl/7.13.2 OpenSSL/0.9.7e zlib/1.2.2 libidn/0.5.13 Accept: */* ... < HTTP/1.1 200 OK < Content-Location: attaque-dns-via-orn.gif < Content-Type: image/gif ...
Le serveur HTTP répond en suivant ses propres préférences, ici, il
envoie du GIF (Apache utilise un algorithme complexe pour
cela, détaillé en http://httpd.apache.org/docs/2.0/content-negotiation.html#methods
;
ici, c'est la petite taille du fichier GIF qui a été déterminante). On
peut orienter les choix faits par Apache comme indiqué en http://httpd.apache.org/docs/2.0/content-negotiation.html#negotiation
.
Ce mécanisme est pénible : il faut choisir une extension pour les
fichiers ainsi servis (la documentation d'Apache suggère
.var
) et que les URL contiennent cette extension
(la documentation d'Apache prétend que Note also that a
typemap file will take precedence over the filename's extension, even
when Multiviews is on ce que mes essais ne confirment
pas). Ensuite, il faut, pour chaque fichier,
un fichier EXEMPLE.var
contenant les priorités, par exemple :
URI: toto URI: toto.dot Content-type: text/plain; qs=0.8 URI: toto.gif Content-type: image/gif; qs=0.9
C'est très surprenant mais je n'ai trouvé aucun moyen de mettre des préférences globales, communes à tous les fichiers comme « Préfères le PNG au GIF ». Pascal Courtois a trouvé une option peu documentée de la directive AddType d'Apache, qui permettrait de faire cela :
AddType image/jpeg;qs=0.7 jpg jpeg AddType image/gif;qs=0.5 gif
et le JPEG sera alors préféré au GIF.
Et avec un vrai navigateur, comment savoir ce qu'il demande ? Pour savoir ce qu'il a envoyé au serveur, on peut utiliser une fonction spécifique du navigateur (par exemple, l'extension Live HTTP Headers de Firefox), ou bien interposer sur le trajet un relais HTTP qui va noter la session (par exemple Muffin) ou bien encore faire enregistrer les en-têtes envoyés par le navigateur par un CGI spécialisé comme ce programme.
On voit alors qu'Internet Explorer 6
envoie image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*
ce qui fait qu'il recevra le GIF en
premier. lynx, lui, envoie le plus compliqué
text/html, text/plain, text/xml, application/x-tex,
application/xml, text/rtf, text/richtext, application/pdf,
application/vnd.sun.xml.writer, application/msword, application/ppt,
application/msexcel, application/x-dia-diagram,
application/x-troff-man, application/postscript,
application/ghostview, image/avs, image/bie, image/x-ms-bmp,
image/cmyk, image/dcx, image/eps, image/fax, image/fits, image/gif,
image/gray, image/gradation, image/hdf, image/jpeg, image/pjpeg,
image/map, image/miff, image/mono, image/mtv, image/x-portable-bitmap,
image/pcd, image/pcx, image/pdf, image/x-portable-graymap, image/pict,
image/png, image/x-portable-anymap, image/x-portable-pixmap, image/ps,
image/rad, image/x-rgb, image/rgba, image/rla, image/rle, image/sgi,
image/sun-raster, image/targa, image/tiff, image/uyvu, image/vid,
image/viff, image/x-xbitmap, image/x-xpixmap, image/x-xwindowdump,
image/yuv, image/svg+xml, image/svg, image/x-eps, image/x-jng,
image/x-xbm, video/x-mng, message/partial, message/external-body,
application/x-ogg, application/ogg, audio/mpeg, audio/x-mpegurl,
audio/x-ms-wax, audio/x-ms-wma, audio/x-pls, audio/x-scpls,
audio/x-wav, video/mpeg, video/quicktime, video/x-mpeg2, video/x-mpeg,
video/x-ms-afs, video/x-ms-asf, video/x-msvideo, video/x-ms-wma,
video/x-ms-wmv, video/x-ms-wmx, video/x-ms-wvx, application/x-tar,
application/x-gtar, application/rtf, application/x-dvi,
application/x-xfig, text/enriched, application/x-abiword,
application/vnd.ms-word, text/abiword, text/*,
application/x-debian-package, audio/basic, */*;q=0.01
,
assez inutile dans ses détails puisque lynx n'affiche pas les images...
La négociation de contenu est-elle une solution idéale ? Non, il y a des problèmes. Par exemple, à partir d'Apache 2, elle ne fonctionne plus correctement avec les redirections. Si on a une redirection :
Redirect /toto/ http://www.example.com/
Et qu'on demande /toto/
, et s'il existe un fichier
dont le préfixe est toto
, Apache va ajouter le
suffixe avant de tenter la redirection (c'était le contraire en Apache
1) et échouera :
[Mon Jul 09 21:23:45 2007] [error] [client 172.19.1.1] File does not exist: /usr/pkg/share/httpd/htdocs/toto.jpg/
Tous les navigateurs ne gèrent pas sans problème cette négociation de contenu. Par exemple, Google Chrome a des problèmes lorsque l'image est incluse dans une page Web.
Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)