Aller au contenu Aller au menu principal Aller au menu secondaire Aller au pied de page

Utiliser l’API de .fr en Elixir (4/4)

Accueil > Observatoire & ressources > Papiers d’experts > Utiliser l’API de .fr en Elixir (4/4)
Le 16/11/2022

Nous avons présenté le fonctionnement général de l’API de .fr, puis vu comment l’utiliser en pratique avec les langages Python et Go. Voyons maintenant comment l’utiliser dans le langage de programmation Elixir. Attention, pour simplifier cet article, on omettra une partie du code. Une version plus complète du programme figure en https://gitlab.rd.nic.fr/afnic/code-samples/-/tree/main/API/Elixir.

Les choix techniques

Elixir n’a pas en standard de quoi faire du HTTP ou du JSON. On va donc utiliser des modules extérieurs, HTTPoison pour HTTP et Jason pour JSON1. On se servira de l’outil habituel mix pour gérer ces dépendances extérieures. Il faut donc écrire un fichier mix.exs :

defmodule Afnic.MixProject do

  use Mix.Project

  def project do

    [

      app: :afnic,

      version: "0.1.0",

      deps: deps()

    ]

  end

  defp deps do

    [

      {:httpoison, "~> 1.7"}, # https://hexdocs.pm/httpoison/

      {:jason, "~> 1.2"} # https://hexdocs.pm/jason/

    ]

  end

end

Et charger les dépendances avant de commencer à travailler, avec mix deps.get.

L’authentification

Ces sous-programmes utilitaires sont placés dans le fichier lib/afnic.exs.

  def get_token() do

    url = "https://login-sandbox.nic.fr/auth/realms/fr/protocol/openid-connect/token"

    body =

      {:form,

       [

         client_id: "registrars-api-client",

         username: login,

         password: password,

         grant_type: "password"

       ]}

    headers = [{"Accept", "application/json"}]

    result = HTTPoison.post!(url, body, headers, [])

    case result do

      %HTTPoison.Response{status_code: 200, body: body} ->

        response = Jason.decode!(body)

        response["access_token"]

      other ->

        raise "Boom (#{inspect(other)})"

    end

  end

Ce code définit une fonction qui va faire une requête HTTP avec la méthode POST pour obtenir le jeton2. Elle va pour cela passer une série de couples clé-valeur3 indiquant notamment le mot de passe du client. Le résultat, après correspondance de motif (pour vérifier le code de retour 200), est analysé avec les fonctions du module Jason. Une fois obtenu le jeton (c’est un des membres de l’objet JSON renvoyé, access_token), on peut faire une liste des champs nécessaires pour les en-têtes des futures requêtes HTTP :

def get_headers() do

    token = get_token()

    [

      {"Accept", "application/json"},

      {"Content-Type", "application/json"},

      {"Extensions", "FRNIC_V2"},

      {"Authorization", "Bearer #{token}"}

    ]

  end

Les erreurs qui peuvent se produire

Dans certains cas, l’API va utiliser les codes de retour HTTP pour signaler un problème. Ainsi, tenter de détruire un domaine qui n’existe pas renverra le fameux 404, objet non trouvé. C’est pour cela qu’on utilise la correspondance de motif (case result do…) pour vérifier le code de retour. S’il indique une erreur, on lève une exception, inspect nous donnant les détails4.

Liste de nos domaines

url = "https://api-sandbox.nic.fr/v1/domains"

result = HTTPoison.get!(url, Afnic.get_headers(), [])

responses =

  case result do

    %HTTPoison.Response{status_code: 200, body: body} ->

      response = Jason.decode!(body)

      response["content"]

    other ->

      raise "Boom (#{inspect(other)})"

  end

Enum.each(responses, fn j -> IO.puts(j["name"]) end)

On fait une requête GET à l’URI de gestion des domaines, en ajoutant les en-têtes nécessaires. Il n’y a pas d’arguments à cette requête. Le résultat est du JSON, qu’on analyse, le transformant en un énumérable Elixir. On n’a plus qu’à appliquer une fonction à tous les éléments de cet énumérable, avec Enum.each. La fonction (qui est anonyme) affiche chaque nom de domaine que nous gérons. Exécuter le programme avec mix run list-domains.exs montrera donc la liste de nos domaines.

Recherche de disponibilité

On nomme souvent ce service DAS, pour Domain Availability Service.

list = Jason.encode!(%{names: System.argv()})

url = "https://api-sandbox.nic.fr/v1/domains/check"

result = HTTPoison.post!(url, list, Afnic.get_headers(), [])

responses =

  case result do

    %HTTPoison.Response{status_code: 200, body: body} ->

      response = Jason.decode!(body)

      response["response"]

    other ->

      raise "Boom (#{inspect(other)})"

  end

Enum.map(responses, fn j ->

  avail =

    if j["available"] do

      "available"

    else

      "NOT available (#{j["reason"]})"

    end

 

  IO.puts("#{j["name"]}: #{avail}")

end)

La liste des noms de domaine qui nous intéressent est obtenue sur la ligne de commande (le System.argv). On en fait un dictionnaire Elixir, que Jason.encode! encode à la syntaxe JSON. La requête est cette fois de méthode HTTP POST. La réponse est un objet JSON, qu’on analyse,  avant d’afficher, toujours avec each et une fonction anonyme, les informations sur chaque nom :

% mix run das.exs toto.fr nic.fr truc.com

nic.fr: NOT available (IN_USE)

toto.fr: available

truc.com: NOT available (ZONE_UNKNOWN)

Un nom est enregistrable, un autre est déjà utilisé, le dernier étant dans un autre domaine de tête. Notez que des détails supplémentaires sont renvoyés dans la réponse en JSON, par exemple pour les noms de domaine nécessitant un examen préalable. Ces détails ne sont pas utilisés ici.

Création d’un domaine

url = "https://api-sandbox.nic.fr/v1/domains"

domain = List.first(System.argv())

body =

  Jason.encode!(%{

    name: domain,

    authorizationInformation: "Vachement1234sur",

    registrantClientId: contact,

    contacts: [

      %{

        clientId: contact,

        role: "ADMINISTRATIVE"

      },

      %{

        clientId: contact,

        role: "TECHNICAL"

      }

    ]

  })

result = HTTPoison.post!(url, body, Afnic.get_headers(), [])

response =

  case result do

    %HTTPoison.Response{status_code: 201, body: body} ->

      Jason.decode!(body)

    other ->

      raise "Boom (#{inspect(other)})"

  end

IO.puts("#{domain} created on #{response["creationDate"]}")

Ici, le nom de domaine à créer est indiqué sur la ligne de commande (System.argv). La création d’un nom nécessite de passer un objet JSON plus complexe, où on indique, outre ce nom, le « authinfo » qui servira pour authentifier les transferts, et surtout le titulaire du nom et les contacts. Ici, on a choisi de mettre le même identificateur pour le titulaire, le contact technique, et le contact administratif. Cet identificateur est celui obtenu lors de la création d’un contact (non montrée ici), comme « CTC65093 ».

Suppression d’un domaine

domain = List.first(System.argv())

url = "https://api-sandbox.nic.fr/v1/domains/#{URI.encode(domain)}"

result = HTTPoison.delete!(url, Afnic.get_headers(), [])

case result do

    %HTTPoison.Response{status_code: 200, body: body} ->

      Jason.decode!(body)

    other ->

      raise "Boom (#{inspect(other)})"

end

IO.puts("#{domain} deleted")

Les seules nouveautés ici sont l’utilisation de la méthode HTTP DELETE qui, comme son nom l’indique, sert à détruire l’objet visé, et le fait que l’identificateur de cet objet, le nom de domaine, est dans l’URI. Ah, et on ignore la réponse (qui ne contient guère d’informations supplémentaires), d’où le trait bas devant le nom de la variable.

Ajout d’un serveur de noms à tous les domaines

ns = List.first(System.argv())

url = "https://api-sandbox.nic.fr/v1/domains"

result = HTTPoison.get!(url, Afnic.get_headers(), [])

responses =

  case result do

    %HTTPoison.Response{status_code: 200, body: body} ->

      response = Jason.decode!(body)

      response["content"]

    other ->

      raise "Boom (#{inspect(other)})"

  end

Enum.each(responses, fn j ->

  domain = j["name"]

  args = Jason.encode!(%{name: domain, nameServersToAdd: [ns]})

  result = HTTPoison.patch!(url, args, Afnic.get_headers(), [])

  case result do

      %HTTPoison.Response{status_code: 200, body: body} ->

        Jason.decode!(body)

        IO.puts("#{domain} updated")

      %HTTPoison.Response{status_code: 404} ->

        IO.puts("#{domain} does not exist")

      %HTTPoison.Response{status_code: 400, body: body} ->

        response = Jason.decode!(body)

        IO.puts("#{domain} not updated because #{List.first(response["errors"])["message"]}")

      other ->

        raise "Boom (#{inspect(other)})"

  end

end)

On récupère la liste de ses domaines, comme précédemment, en utilisant la méthode GET. Le serveur de noms à ajouter est, lui, indiqué sur la ligne de commande. Cette liste de domaines est mise dans la variable responses, à laquelle on va ensuite appliquer, avec Enum.map, une fonction qui met à jour le domaine, par la méthode HTTP PATCH5.

On notera que, cette fois, le traitement d’erreur est plus détaillé : on réagit au code de retour HTTP 404 par un meilleur message. Pour le 400, indiquant une requête invalide, on décode le JSON renvoyé, pour pouvoir indiquer un message informatif.


1 – Comme pour tout programme, quel que soit le langage de programmation utilisé et quelle que soit la licence – libre ou privatrice – des composants logiciels extérieurs utilisés, il faut prendre en compte sur les risques sur la chaîne d’approvisionnement en logiciel. Les modules extérieurs doivent idéalement n’être chargés que depuis une source de confiance, et on ne doit pas leur faire une confiance aveugle.

2 – post! (avec le point d’exclamation à la fin) lève une exception en cas de problème, au lieu de renvoyer l’atome :error. Même chose pour les autres fonctions ayant ce point d’exclamation.

3 – On n’utilise pas JSON dans la requête mais l’encodage habituel des formulaires Web, d’où l’atome :form au début.

4 – Si on veut des messages d’erreur plus lisibles, par exemple pour des utilisateurs finals, on peut récupérer le corps de la réponse, du JSON qui contient des informations sur ce qui n’a pas marché. Par exemple, en cas de mot de passe erroné, on récupérera {« error »: »invalid_grant », »error_description »: »Invalid user credentials »}.

5 – Normalisée à l’origine dans le RFC 5789.