Date de publication du RFC : Janvier 2021
Auteur(s) du RFC : M. Loffredo (IIT-CNR/Registro.it), M. Martinelli (IIT-CNR/Registro.it), S. Hollenbeck (Verisign Labs)
Chemin des normes
Réalisé dans le cadre du groupe de travail IETF regext
Première rédaction de cet article le 24 janvier 2021
Le protocole RDAP, normalisé notamment dans les RFC 9082 et RFC 9083, permet de récupérer des informations structurées sur des trucs (oui, j'ai écrit « trucs ») enregistrés auprès d'un registre, par exemple des domaines auprès d'un registre de noms de domaine. Voici un RFC tout juste publié qui ajoute à RDAP la possibilité de trier les résultats et également de les afficher progressivement (paging). C'est évidemment surtout utile pour les requêtes de type « recherche », qui peuvent ramener beaucoup de résultats.
Avec des requêtes « exactes » (lookup dans le RFC 9082), le problème est moins grave. Ici, je cherche juste de l'information sur un nom de domaine et un seul :
% curl https://rdap.nic.bzh/domain/chouchen.bzh ... "events" : [ { "eventDate" : "2017-07-12T10:18:12Z", "eventAction" : "registration" }, { "eventDate" : "2020-07-09T09:49:06Z", "eventAction" : "last changed" }, ...
Mais si je me lançais dans une recherche plus ouverte
(search dit le RFC 9082),
avec par exemple la requête domains
(notez le S
à la fin, cf. RFC 9082, section 3.2.1), le
nombre de résultats pourrait être énorme. Par exemple, si je
demandais tous les domaines en
.bzh
avec
https://rdap.nic.bzh/rdap/domains?name=*.bzh
,
j'aurais une réponse d'une taille conséquente. (Et je ne vous dis pas
pour .com
…)
En pratique, cette requête ne fonctionnera pas car je ne connais aucun registre qui autorise les recherches RDAP aux utilisateurs anonymes. Ceux-ci ne peuvent faire que des requêtes exactes, à la fois pour épargner les ressources informatiques (cf. la section 7 du RFC sur la charge qu'impose les recherches), et pour éviter de distribuer trop d'informations à des inconnus pas toujours bien intentionnés. Pour effectuer ces recherches, il faut donc un compte et une autorisation. Autrement, vous récupérez un code 401, 403 ou bien carrément une liste vide.
Et si vous avez une telle autorisation, comment gérer une masse importante de résultats ? Avec le RDAP originel, vous récupérez la totalité des réponses, que vous devrez analyser, et ce sera à vous, client, de trier. (Si le serveur n'envoie qu'une partie des réponses, pour épargner le client et ses propres ressources, il n'a malheureusement aucun moyen de faire savoir au client qu'il a tronqué la liste.) C'est tout le but de notre RFC que de faire cela côté serveur. Des nouveaux paramètres dans la requête RDAP vont permettre de mieux contrôler les réponses, ce qui réduira les efforts du client RDAP, du serveur RDAP et du réseau, et permettra d'avoir des résultats plus pertinents.
La solution ? La section 2 de notre RFC décrit les nouveaux paramètres :
count
: le client demande à être
informé du nombre de trucs que contient la liste des réponses.sort
: le client demande à trier les
résultats.cursor
: ce paramètre permet d'indiquer
un endroit particulier de la liste (par exemple pour la récupérer
progressivement, par itérations successives).
Par exemple,
https://example.com/rdap/domains?name=example*.com&count=true
va récupérer dans .com
(si le registre de
.com
acceptait cette requête…) tous les noms de
domaine dont le nom commence par example
, et
indiquer leur nombre. Une réponse serait, par exemple :
"paging_metadata": { "totalCount": 43 }, "domainSearchResults": [ ... ]
(paging_metadata
est expliqué plus loin.)
Pour le paramètre sort
, le client peut
indiquer qu'il veut un tri, sur quel critère se fait le tri, et que
celui-ci doit être dans l'ordre croissant (a
pour ascending) ou décroissant
(d
pour descending). Ainsi,
https://example.com/rdap/domains?name=*.com&sort=name
demande tous les noms en .com
triés par
nom. https://example.com/rdap/domains?name=*.com&sort=registrationDate:d
demanderait tous les noms triés par date d'enregistrement, les plus
récents en premier.
L'ordre de tri dépend de la valeur JSON du résultat (comparaison
lexicographique
pour les chaînes de caractères et numérique pour les nombres), sauf
pour les adresses IP, qui
sont triées selon l'ordre des adresses et pour les dates qui sont
triées dans l'ordre chronologique. Ainsi, l'adresse
9.1.1.1
est inférieure à
10.1.1.1
(ce qui n'est pas le cas dans l'ordre
lexicographique). Le RFC fait remarquer que tous les SGBD
sérieux ont des fonctions pour traiter ce cas. Ainsi, dans
PostgreSQL, la comparaison des deux chaînes
de caractères donnera :
=> SELECT '10.1.1.1' < '9.1.1.1'; t
Alors que si les adresses IP sont mises dans une colonne ayant le
type correct (INET
), on a le bon résultat :
=> SELECT '10.1.1.1'::INET < '9.1.1.1'::INET; f
Après le sort=
, on trouve le nom de la
propriété sur laquelle on trie. C'est le nom d'un membre de l'objet
JSON de la réponse. Non, en fait, c'est plus compliqué que
cela. Certains membres de la réponse ne sont pas utilisables (comme
roles
, qui est multi-valué) et des informations
importantes (comme registrationDate
cité en
exemple plus haut) ne sont pas explicitement dans la réponse. Notre
RFC définit donc une liste de propriétés utilisables, et explique
comment on les calcule (par exempe,
registrationDate
peut se déduire des
events
). Plutôt que ces noms de propriétés, on
aurait tout pu faire en JSONpath ou JSON
Pointer (RFC 6901) mais ces deux syntaxes sont
complexes et longues
($.domainSearchResults[*].events[?(@.eventAction='registration')].eventDate
est le JSONPath pour registrationDate
). La
mention en JSONPath du critère de tri est donc facultative.
Et, bien sûr, si le client envoie un nom de propriété qui n'existe pas, il récupérera une erreur HTTP 400 avec une explication en JSON :
{ "errorCode": 400, "title": "Domain sorting property 'unknown' is not valid", "description": [ "Supported domain sorting properties are:" "'aproperty', 'anotherproperty'" ] }
Et le troisième paramètre, cursor
? Ce RFC
fournit deux méthodes pour indiquer où on en est dans la liste des
résultats, la pagination par décalage (offset
pagination) et celle par clé (keyset
pagination, qu'on trouve parfois
citée sous le nom ambigu de cursor
pagination, qui désigne plutôt une méthode avec état sur
le serveur). Ces deux méthodes ont en commun de ne
pas nécessiter d'état du côté du serveur. La
pagination par décalage consiste à fournir un décalage depuis le
début de la liste et un nombre d'éléments désiré, par exemple
« donne-moi 3 éléments, commençant au numéro 10 ». Elle est simple à
mettre en œuvre, par exemple avec SQL :
SELECT truc FROM Machins ORDER BY chose LIMIT 3 OFFSET 9;
Mais elle n'est pas forcément robuste si la base est modifiée pendant ce temps : passer d'une page à l'autre peut faire rater des données si une insertion a eu lieu entretemps (cela dépend aussi de si la base est relue à chaque requête paginée) et elle peut être lente (surtout avec RDAP où la construction des réponses prend du temps, alors que celles situées avant le début de la page seront jetées). L'autre méthode est la pagination par clé où on indique une caractéristique du dernier objet vu. Si les données sont triées, il est facile de récupérer les N objets suivants. Par exemple en SQL :
SELECT truc FROM Machins WHERE chose > [la valeur] ORDER BY chose LIMIT 3;
Un inconvénient de cette méthode est qu'il faut un champ (ou un
ensemble de champs) ayant un ordre (et pas de duplicata). RDAP rend
cela plus difficile, en agrégeant des informations provenant de
différentes tables (cf. l'annexe B du RFC). (Voir des descriptions
de cette pagination par clé dans « Paginating
Real-Time Data with Keyset Pagination » ou
« Twitter
Ads API », pour Twitter.)
RDAP permet les deux méthodes, chacune ayant ses avantages et ses
inconvénients. L'annexe B du RFC explique plus en détail ces
méthodes et les choix faits. (Sinon, en dehors de RDAP, un bon
article sur le choix d'une méthode de pagination, avec leur mise en
œuvre dans PostgreSQL est « Five
ways to paginate in Postgres, from the basic to the
exotic ».) Pour RDAP, un
https://example.com/rdap/domains?name=*.com&cursor=offset:9,limit:3
récupérerait une page de trois éléments, commençant au dixième, et
https://example.com/rdap/domains?name=*.com&cursor=key:foobar.com
trouverait les noms qui suivent foobar.com
.
Notez qu'en réalité, vous ne verrez pas directement le décalage, la
clé et la taille de la page dans l'URL : ils sont encodés pour
permettre d'utiliser des caractères quelconques (et aussi éviter que
le client ne les bricole, il est censé suivre les liens, pas
fabriquer les URL à la main). Les exemples du RFC utilisent
Base64 pour l'encodage, en notant qu'on peut
certainement faire mieux.
La capacité du serveur à mettre en œuvre le tri et la pagination
s'indiquent dans le tableau rdapConformance
avec les chaînes sorting
et
paging
(qui sont désormais dans le
registre IANA). Par exemple (pagination mais pas tri) :
"rdapConformance": [ "rdap_level_0", "paging" ]
Il est recommandé que le serveur documente ces possibilités dans
deux nouveaux éléments qui peuvent être présents dans une réponse,
sorting_metadata
et
paging_metadata
. Cela suit les principes
d'auto-découverte de HATEOAS. Dans la
description sorting_metadata
, on a
currentSort
qui indique le critère de tri
utilisé, et availableSorts
qui indique les
critères possibles. Chaque critère est indiqué avec un nom
(property
), le fait qu'il soit le critère par
défaut ou pas, éventuellement une expression JSONPath
désignant le champ de la réponse utilisé et enfin une série de liens
(RFC 8288) qui vont nous indiquer les URL à
utiliser. Pour paging_metadata
, on a
totalCount
qui indique le nombre d'objets
sélectionnés, pageSize
qui indique le nombre
récupérés à chaque itération, pageNumber
qui
dit à quelle page on en est et là encore les liens à suivre. Ce
pourrait, par exemple, être :
"sorting_metadata": { "currentSort": "name", "availableSorts": [ { "property": "registrationDate", "jsonPath": "$.domainSearchResults[*].events[?(@.eventAction==\"registration\")].eventDate", "default": false, "links": [ { "value": "https://example.com/rdap/domains?name=example*.com&sort=name", "rel": "alternate", "href": "https://example.com/rdap/domains?name=example*.com&sort=registrationDate", "title": "Result Ascending Sort Link", "type": "application/rdap+json" }, { "value": "https://example.com/rdap/domains?name=example*.com&sort=name", "rel": "alternate", "href": "https://example.com/rdap/domains?name=example*.com&sort=registrationDate:d", "title": "Result Descending Sort Link", "type": "application/rdap+json" } ] ...
Ici, sorting_metadata
nous indique que le tri
se fera sur la base du nom, mais nous donne les URL à utiliser pour
trier sur la date d'enregistrement. Quant à la pagination, voici un
exemple de réponse partielle, avec les liens permettant de récupérer
la suite :
"paging_metadata": { "totalCount": 73, "pageSize": 50, "pageNumber": 1, "links": [ { "value": "https://example.com/rdap/domains?name=example*.com", "rel": "next", "href": "https://example.com/rdap/domains?name=example*.com&cursor=wJlCDLIl6KTWypN7T6vc6nWEmEYe99Hjf1XY1xmqV-M=", "title": "Result Pagination Link", "type": "application/rdap+json" } ]
L'idée de permettre le contrôle des réponses via des nouveaux
paramètres (count
, sort
et
cursor
, présentés ci-dessus), vient entre autre
du protocole OData. Une autre solution aurait
été d'utiliser des en-têtes HTTP (RFC 7231). Mais
ceux-ci ne sont pas contrôlables depuis le
navigateur, ce qui aurait réduit le nombre de
clients possibles. Et puis cela rend plus difficile
l'auto-découverte des extensions, qui est plus pratique via des
URL, cette
auto-découverte étant en général considérée comme une excellente
pratique REST.
Il existe à l'heure actuelle une seule mise en œuvre de ce RFC
dans le RDAP non public de .it
.
La documentation est en ligne.
Notre RFC permet donc de ne récupérer qu'une partie des objets qui correspondent à la question posée. Si on veut plutôt récupérer une partie seulement de chaque objet, il faut utiliser le RFC 8982.
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)