Greboca  

Blog de Stéphane Bortzmeyer  -  RFC 9205: Building Protocols with HTTP

 -  10 juin - 

Aujourd'hui, la grande majorité des API accessibles via le réseau fonctionnent au-dessus de HTTP. Ce nouveau RFC, qui remplace le RFC 3205, décrit les bonnes pratiques pour la conception de telles API, notamment pour les protocoles IETF bâtis sur HTTP, comme DoH ou RDAP.

Il y a plein d'applications qui fonctionnent au-dessus de HTTP. Ce nouveau RFC se concentre sur celles qui sont d'usage général et qui ont plusieurs mises en œuvre et déploiements. (Si vous faites un service centralisé qui n'a qu'un seul déploiement de son API spécifique, ce RFC ne va pas forcément être pertinent pour vous.) Si vous avez déjà lu le RFC 3205, il faudra tout recommencer, les changements sont nombreux. Ces applications utilisant HTTP sont parfois qualifiées de REST mais, en toute rigueur, toutes ne suivent pas rigoureusement les principes de REST. Notez aussi un sous-ensemble, CRUD, pour les applications dont l'essentiel du travail est de créer/supprimer/gérer des objets distants.

Normalement, HTTP avait été conçu pour le Web et ses usages. Mais on voit aujourd'hui de très nombreuses API réseau être fondées sur HTTP pour diverses raisons :

  • Parce que le développeur ou la développeuse ne connait que ça,
  • parce qu'il existe un très grand nombre de bibliothèques pour faire les clients (comme l'excellente Requests pour le langage Python, qui sera utilisée ici dans plusieurs exemples), et de cadriciels pour les serveurs,
  • parce que dans certains cas, il sera même possible d'utiliser un navigateur Web pour y accéder, et que tout le monde a un navigateur Web et est familier avec ce logiciel,
  • parce que certains services comme l'authentification sont déjà disponibles dans les logiciels existants,
  • parce que HTTP est le seul protocole dont on peut être sûr qu'il passera même dans les réseaux les plus coincés comme les hotspots des hôtels et des aéroports.
Mais tout n'est pas forcément rose et HTTP peut ne pas être bien adapté à ce qu'envisage le développeur de l'API. Et cette développeuse peut faire des erreurs lors de la conception de l'API, erreurs que ce RFC vise à éviter.

En effet, quand on développe une API sur HTTP, il y a plusieurs décisions à prendre :

  • Définir un nouveau plan d'URL ? (C'est quand même rare, presque tout le monde utilise http: ou, aujourd'hui, https:.)
  • Utiliser un port « non-standard » ?
  • Comment coexister avec les autres utilisations de HTTP, comme la navigation sur le Web ?

La section 2 précise l'applicabilité de ce RFC. Il concerne les protocoles qui utilisent HTTP (ports 80 ou 443, plans d'URI http: ou https:). Ceux qui utiliseraient une version modifiée de HTTP ne comptent pas, et cette pratique est d'ailleurs déconseillée, puisque ces versions modifiées feraient probablement perdre les avantages d'utiliser HTTP, notamment la réutilisation des logiciels et infrastructures existants.

Section 3, maintenant. Quelles sont les caractéristiques importantes de HTTP, qui gouvernent ce que peuvent faire les applications qui l'utilisent ? D'abord, sa sémantique très générale : on peut tout faire avec HTTP. Notammment, HTTP est indépendant du type de ressources sur lesquelles il agit. Ainsi, des composants HTTP génériques (bibliothèques, serveurs, relais) peuvent être développés et déployés pour des applications très différentes, même des applcations qui n'existent pas encore. (Voilà d'alleurs pourquoi la section précédente insistait sur le fait q'il ne faut pas modifier HTTP.) Plus subtile serait l'erreur qui consisterait à spécifier un certain profil de HTTP, en restreignant ce que HTTP peut faire ou pas (« la réponse à un POST doit être 201 »). Une telle restriction, là encore, empêcherait d'utiliser cerrtains composants génériques, en faisant perdre à HTTP de sa généralité.

Une autre erreur courante est de s'attribuer tout ou partie de l'espace de nommage fourni par les liens hypertextes. C'est par exemple le cas lorsqu'une application estime qu'elle peut contrôler tout le chemin dans l'URI et décider que /truc/machin est forcément à elle (RFC 8820). Cela complique le déploiement, par exemple si on veut installer cette application sous /chose et excusivement sous ce chemin (cf. section 4.4). L'application devrait au contraire permettre de la souplesse et utiliser les possibilités qu'offre le système de liens (RFC 8288).

Enfin, HTTP dispose de nombreuses possibilités comme le multiplexage que permettent HTTP/2 (RFC 9113) et HTTP/3 (RFC 9114), l'intégration avec TLS, la possibilité de relayage, la négociation de contenu, la disponibilité de nombreux clients, et l'application qui utilise HTTP doit donc veiller à ne pas casser cet écosystème, et en tout cas à ne pas réinventer la roue, alors que HTTP offre déjà de nombreuses solutions éprouvées.

Bref, compte-tenu de tout cela, comment faire pour bien utiliser HTTP dans sa nouvelle application ? La section 4 est là pour répondre à cette question.

D'abord, bien définir la dépendance de l'application à HTTP, en donnant comme référence RFC 9110 (et surtout pas une version spécifique de HTTP, toujours afin de profiter au maximum de l'écosystème existant). On notera quand même que DoH (RFC 8484) impose (section 5.2 de son RFC) au moins HTTP/2, pour être sûr d'avoir du multiplexage. Notre RFC permet explicitement ce genre d'exceptions.

Le RFC recommande également, quand on montre un dialogue HTTP titre d'exemple, d'utiliser plutôt les conventions de HTTP/1 (RFC 9112), plus lisibles. Donc, par exemple, GET /truc HTTP/1.1 plutôt que le :method = GET :path = /resource de HTTP/2. C'est ce que fait curl :


% curl -v http://www.example
> GET / HTTP/2
> Host: www.example
> user-Agent: curl/7.68.0
> accept: */*
> 
< HTTP/2 200 
< content-Type: text/plain
< content-Length: 13

  

On l'a dit, il ne faut pas modifier le comportement de base de HTTP. Ce qu'on peut faire, par contre :

  • Spécifier le type des données (RFC 6838), par exemple JSON (qu'utilise RDAP),
  • spécifier des champs dans l'en-tête,
  • spécifier comment on trouve les ressources via des liens (RFC 8288).
Et le client ? L'application qui utilise HTTP ne devrait pas exiger de comportement trop spécifique du client ; idéalement, un navigateur Web normal devrait pouvoir interagir avec l'application. On peut par exemple s'appuyer sur les principes FETCH. Il est également préférable que l'application qui va utiliser HTTP soit claire sur le traitement attendu pour les redirections HTTP, ou pour les cookies (RFC 6265), et rappeler que la vérification des certificats doit se faire selon les principes de la section 4.3.4 du RFC 9110.

Le client doit, idéalement, pouvoir se configurer avec uniquement un URL. (Par exemple, un serveur DoH est annoncé ainsi, comme https://doh.bortzmeyer.fr/, la seule information dont vous avez besoin pour l'utiliser.) Et si on ne connait qu'un nom de domaine ? La solution du chemin d'URL fixe qu'on s'alloue (« obligatoirement /app ») étant interdite (RFC 8820), il y a le choix :

  • Utiliser un chemin sous /.well-known (RFC 8615),
  • Utiliser les gabarits du RFC 6570, pour générer les URI (un tel mécanisme de découverte est plus souple mais sans doute moins rapide).

Et le plan d'URI (le premier composant de l'URI) ? http: et surtout https: sont évidemment recommandés mais on peut aussi choisir un plan spécifique. Cela va évidemment rendre l'application inutilisable par un navigateur Web ordinaire. Certains navigateurs permettent d'enregistrer un mécanisme de gestion de ces plans non standards (comme le registerProtocolHandler() du WHATWG) mais cela ne marche pas partout. Et on aura le même problème avec tout l'écosystème logiciel de HTTP. Bref, utiliser un plan autre que http: ou https: fera perdre une bonne partie des avantages qu'il y avait à utiliser le protocole HTTP. D'autres problèmes se poseront comme l'impossibilité d'utiliser le concept d'origine (RFC 6454) par exemple dans la Same Origin Policy, ou comme d'autres fonctions utiles de HTTP (cookies, authentification, mémorisation - RFC 9111, HSTS - RFC 6797, etc). Si vous tenez encore, après tout ça, à créer un plan à vous, consultez le RFC 7595.

Et les ports ? HTTP utilise par défaut les ports 80 pour le trafic en clair et 443 pour le traffic chiffré. Utiliser un autre port est possible (https://machin.example:666) mais rend le trafic de l'application distinguable, ce qui peut être gênant pour la vie privée. (C'est un des choix de conception de DoH que d'utiliser HTTPS sur le port 443, pour ne pas être distinguable, et donc être plus difficile à filtrer.) Le RFC 7605 donne des détails sur ce choix des ports.

Maintenant, quelles méthodes HTTP utiliser ? Le RFC exige que les applications utilisant HTTP se servent uniquement des méthodes enregistrées, comme GET ou PUT. Certes, une procédure existe pour enregistrer de nouvelles méthodes mais l'IETF insiste que ces nouvelles méthodes doivent être génériques, et non pas limitées aux besoins d'une seule application. (Le RFC 4791 avait créé des méthodes spécifiques, mais c'était avant. C'est maintenant interdit.)

Donc, pas de méthodes nouvelles. Mais quelle(s) méthode(s) utiliser ? GET est le choix le plus évident. Cette méthode est idempotente (et permet donc, entre autres, la mémorisation), et a une sémantique simple et compréhensible. Elle a quelques limites (comme le fait que tous les éventuels paramètres doivent être dans l'URL, ce qui peut nécessiter un encodage spécial, et peut empêcher des paramètres de grande taille) mais rien de bien grave. Si c'est trop gênant pour une application donnée, il ne reste plus qu'à utiliser POST.

Et pour récupérer des métadonnées sur le service ? Le RFC note que la méthode OPTIONS n'est pas très pratique, par exemple parce qu'elle ne permet pas de donner comme documentation un simple URL (la méthode par défaut étant GET). Il recommande plutôt un URL dans /.well-known (RFC 8615), en créant un nouveau nom, ou bien avec les URL host-meta (RFC 6415). Pour des métadonnées sur une ressource particulière, il est recommandé d'utiliser les liens (RFC 8288). Le RFC note que, dans ce dernier cas, l'en-tête Link: marche même avec la méthode HEAD donc pas besoin de récupérer la ressource pour avoir des informations sur ses métadonnées.

Et les codes de retour HTTP comme 403 ou 404, comment les utiliser ? D'abord, une application qui utilise HTTP n'a pas forcément un complet contrôle sur ces codes de retour, qui peuvent être générés par des composants logiciels différents. Donc, le client HTTP doit se méfier, le code reçu n'est pas forcément significatif de l'application. Ensuite, une application peut avoir davantage de messages différents qu'il n'existe de codes de retour HTTP, ce qui peut pousser à de mauvaises pratiques, comme l'utilisation de codes de retour non standard, ou commme l'utilisation de codes certes standard mais utilisés d'une manière très éloignée de ce qui était prévu. Bref, le RFC conseille de découpler les erreurs applicatives des erreurs HTTP, de ne pas chercher à tout prix un code de retour HTTP pour chaque erreur applicative, et de ne pas hésiter à utiliser les codes de retour génériques (comme 500, pour « quelque chose ne va pas dans le serveur »). Pour envoyer des informations plus détaillées sur l'erreur, il est préférable d'utiliser les techniques du RFC 7807. Autre avertissement du RFC, les raisons envoyées par le serveur après un code de retour (404 File not found) ne sont pas significatives. Dans certains cas (message HTTP dans un message HTTP), elles ne sont pas transmises du tout, contrairement au code de retour, la seule information sur laquelle on peut compter. L'application ne doit donc pas espérer que le client recevra ces raisons.

Autre difficulté pour le concepteur ou la conceptrice d'applications utilisant HTTP, les redirections. Il y a quatre redirections différentes en HTTP, chacune pouvant être temporaire ou définitive, et permettant de changer de méthode ou pas (une requête POST indiquant une redirection suivie d'une requête GET par le client). On a donc :

  • 301, définitive et permettant de changer de méthode,
  • 302, temporaire et permettant de changer de méthode,
  • 307, temporaire et ne permettant pas de changer de méthode,
  • 308, définitive et ne permettant pas de changer de méthode.
Et il faut donc réfléchir un peu avant de choisir un code de redirection (le RFC privilégie 301 et 302, plus souples).

Et les champs dans l'en-tête ? Une application a souvent envie d'en ajouter, que ce soit dans la requête ou dans la réponse. Mais le RFC n'est pas très chaud, demandant qu'on mette les informations plutôt dans l'URL ou dans le corps du message HTTP. Si on crée de nouveaux en-têtes, en théorie (c'est très théorique…), il faut les enregistrer à l'IANA (RFC 9110, section 16.3). Si ces en-têtes ont une structure, il est très recommandé qu'elles suivent les règles du RFC 8941.

Et le corps du message, justement ? L'application doit spécifier quel format est attendu. C'est souvent JSON (RFC 8259) mais cela peut être aussi XML, CBOR (RFC 8949), etc.

Une des grandes forces d'HTTP est la possibilité de mémorisation, décrite en détail dans le RFC 9111. La mémorisation améliore les performances, rend le service moins sensible aux perturbations, et permet le passage à l'échelle. Les applications qui utilisent HTTP ont donc tout intérêt à permettre et à utiliser cette mémorisation. Il est donc recommandé d'indiquer dans la réponse une durée de vie, de préférence avec Cache-Control: max-age=… ou bien, si c'est nécessaire, d'indiquer explicitement que la réponse ne doit pas être mémorisée (Cache-Control: no-store).

Un autre avantage pour une application d'utiliser HTTP est l'existence d'un cadre général pour l'authentification (RFC 9110, section 11). Attention, certains mécanismes ne doivent être utilisés qu'au-dessus d'HTTPS, comme la basic authentication du RFC 7617. HTTPS permet également d'utiliser les certificats client pour l'authentification. (Attention, avec TLS ≤ 1.2, ces certificats, qui contiennent des données personnelles, sont transmis en clair.)

Conséquence de l'utilisation de HTTP, l'application est utilisable via un navigateur Web. Cela peut être vu comme un avantage (tout le monde a un navigateur Web sous la main) ou comme un inconvénient (si la sémantique de l'application ne permet pas réellement un usage pratique depuis un navigateur). Mais quoi qu'on en pense, l'application sera accessible aux navigateurs, et il est donc important de s'assurer que cela ne provoquera pas de problème. Par exemple, si on peut changer un état avec une requête POST, l'application pourrait être attaquée assez facilement par CSRF. Si l'application tire une partie des données qu'elle renvoie en réponse d'une source que l'attaquant peut contrôler, on risque le XSS. Il est donc recommandé, même si l'application n'est pas prévue pour être utilisée par un navigateur, de suivre les mêmes règles de développement sécurisé que si elle devait être accédée depuis un navigateur, notamment :

  • utiliser des champs dans l'en-tête comme X-Content-Type-Options: nosniff,
  • utiliser CSP,
  • utiliser Referrer-Policy:,
  • utiliser l'option HttpOnly sur les cookies (RFC 6265, section 5.2.6).
Voici un exemple d'une réponse suivant ces principes :
HTTP/1.1 200 OK
Content-Type: application/example+json
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'
Cache-Control: max-age=3600
Referrer-Policy: no-referrer    
  

Il reste à régler la question des frontières de l'application. Le plus simple pour l'application est d'avoir son propre nom de domaine et donc une origine (RFC 6454) unique. Cela simplifie par exemple des problèmes comme les cookies. Mais cela complique le déploiement, empêchant de mettre plusieurs applications derrière le même nom. Le RFC conseille donc plutôt de concevoir des applications pouvant coexister avec d'autres applications sous le même nom (RFC 8820).

Un mot sur la sécurité pour finir (section 6 du RFC). D'abord, une application qui utilise HTTP va évidemment hériter des questions de sécurité générale de HTTP, comme détaillées dans la section 17 du RFC 9110. Vu le caractère sensible des données traitées par beaucoup d'applications, le RFC recommande l'utilisation de HTTPS. Mais il développe aussi la question de la vie privée. HTTP est très bavard et le serveur en apprend beaucoup, souvent beaucoup trop, sur son client. Ainsi, les cookies, l'adresse IP source, les ETags, les tickets de session TLS sont très utiles au serveur qui voudrait suivre un client. Et le RFC rappelle que HTTP donne assez d'informations « auxiliaires » pour pouvoir reconnaitre un client (ce qu'on nomme le fingerprinting). Bref, le maintien de son intimité va être aussi difficile que sur le Web.

Ce RFC remplace l'ancien RFC 3205. Comme le note l'annexe A de notre RFC, il y a trop de changements pour les lister ; ce document est très différent de son prédécesseur (qui date de 2002 !).

Voyons maintenant quelques exemples d'application utilisant HTTP. Dans le monde IETF, il y a évidemment RDAP (RFC 7480, RFC 9082 et RFC 9083). RDAP suit bien les principes de ce RFC. Par exemple, les chemins d'URL comme /domain ou /ip ne sont pas forcément à la « racine » du serveur HTTP. Autre exemple, DoH (RFC 8484), également fidéle (heureusement !) aux recommandations de l'IETF. Notez que ces recommandations laissent des choix. Ainsi, lorsque le nom de domaine cherché n'est pas trouvé, RDAP renvoie le code 404 (RFC 7480, section 5.3) alors que DoH préfère renvoyer un 200 (le serveur HTTP a bien été joignable et a bien répondu), gardant le signal de non-existence uniquement dans la réponse DNS (RFC 8484, section 4.2.1) transportée sur HTTP (l'argument est que DoH ne fait que transporter les requêtes d'un autre protocole, contrairement à RDAP).

J'ai parlé plus haut de la possibilité d'utilisation d'un navigateur Web ordinaire pour accéder aux applications utilisant HTTP. Mais comme ces applications envoient souvent des données structurées en JSON, il faut un navigateur qui gère bien le JSON. Et c'est justement ce que fait Firefox, qui sait l'afficher de manière pratique :

Terminons avec quelques exemples d'API « finales » (donc pas le sujet principal du RFC, qui parle de protocoles IETF). Commençons modestement par l'API du DNS looking glass. A priori, elle suit tous les principes de ce RFC. En tout cas, elle essaie. Mais si vous constatez des différences avec le RFC, n'hésitez pas à faire un rapport. Autre API intéressante, celle des sondes RIPE Atlas. Elle utilise toutes les possibilités de HTTP, notamment les multiples méthodes (DELETE pour supprimer une mesure en cours, par exemple). J'aurais juste trouvé plus logique d'utiliser PUT au lieu de POST pour créer une mesure. L'API de Mastodon (cf. sa documentation) est encore plus incohérente, utilisant POST pour créer un pouète, mais PUT pour le mettre à jour.

par Stéphane Bortzmeyer

Blog de Stéphane Bortzmeyer

Les paquets IP passent-ils vraiment là où on leur dit ?

 -  22 juillet - 

La lecture d'une excellente étude faite à l'université de Twente m'a motivé pour faire un court article sur une question qu'on se pose trop rarement (...)


Le NIST a choisi ses algorithmes de cryptographie post-quantiques

 -  6 juillet - 

Ce mardi 5 juillet 2022, l'organisme de normalisation étatsunien NIST a annoncé qu'il avait choisi les algorithmes de cryptographie post-quantiques (...)


RFC 9255: The 'I' in RPKI Does Not Stand for Identity

 -  15 juin - 

Un très court RFC pour un simple rappel, qui ne devrait même pas être nécessaire : les identités utilisées dans la RPKI, la base des identités qui sert (...)


RFC 9209: The Proxy-Status HTTP Response Header Field

 -  14 juin - 

Le protocole HTTP, qui est à la base du Web, n'est pas forcément de bout en bout, entre client et serveur. Il y a souvent passage par un relais et (...)


RFC 7871: Client Subnet in DNS Queries

 -  13 juin - 

Ce nouveau RFC décrit une option EDNS qui permet à un client DNS d'indiquer au serveur l'adresse IP d'origine de la requête DNS. Pourquoi diable (...)