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

Using the .fr API with Elixir (4/4)

Home > Observatory and resources > Expert papers > Using the .fr API with Elixir (4/4)
11/16/2022

We’ve presented the general workings of the .fr API and we’ve seen how to use it in practice with the Python and Go languages. Let’s see now how to use it in the Elixir programming language. Warning, to simplify this article we will omit part of the code. A more complete version of the program can be found at https://gitlab.rd.nic.fr/afnic/code-samples/-/tree/main/API/Elixir.

The technical choices

Elixir does not have any HTTP or JSON capability as standard. We will therefore use external modules, HTTPoison for HTTP and Jason for JSON1. We will use the usual mix tool to manage these external dependencies. So we have to write a mix.exs file:

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

And the dependencies must be loaded before starting to work, with mix deps.get.

Authentication

These utility sub-programs are kept in the lib/afnic.exs. file.

  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

This code defines a function that will make a HTTP request using the POST method to obtain the token2. For this it will pass a series of key-value pairs3 indicating in particular the client’s password. The result, after matching the reason (to verify the response status code 200), is analysed with the functions of the Jason module. Once the token has been obtained (it is one of the members of the JSON object sent, access_token), we can make a list of the fields necessary for the headings of the future HTTP requests:

def get_headers() do

    token = get_token()

    [

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

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

      {"Extensions", "FRNIC_V2"},

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

    ]

  end

Errors that may occur

In certain cases, the API will use HTTP response status codes to indicate a problem. For example, trying to destroy a domain that doesn’t exist will return the famous 404, object not found. It is for this reason that we use matching reason (case result do…) to verify the response status code. If it indicates an error, we raise an exception, inspect, giving us the details4.

List of our domains

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)

We make a GET request to the domain management URI by adding the necessary headers. There are no arguments to this request. The result is in JSON, which will be analysed, transforming it into an Elixir enumerable. We now have only to apply a function to all the elements of this enumerable, with Enum.each. The function (which is anonymous) displays each domain name that we manage. Executing the program with mix run list-domains.exs will therefore show the list of our domains.

Availability enquiry

This service is often referred to as DAS, 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)

The list of domain names we’re interested in is obtained from the command line (sys.argv). We make an Elixir map, which Jason encodes in JSON syntax. The request this time uses the HTTP POST method. The response is a JSON object, which is analysed, before being displayed, still with each and an anonymous function, the information on each name:

% mix run das.exs foobar.fr nic.fr stuff.com

nic.fr: NOT available (IN_USE)

foobar.fr: available

stuff.com: NOT available (ZONE_UNKNOWN)

One name is available for registration, another is already in use, the last one name is not available for registration being in another TLD. Note that additional details are sent in the response in JSON, for example for domain names requiring prior examination. These details are not used here.

Creation of a domain

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"]}")

Here, the domain name to be created is indicated on the command line (sys.argv). The creation of a name requires passing a more complex JSON object where we indicate, in addition to this name, the “authinfo” that will serve to authenticate the transfers, and above all the holder of the name and the contacts. Here, we have chosen to use the same identifier for the holder, the technical contact and the administrative contact. This identifier is the one obtained when creating a contact (not shown here), such as “CTC65093”.

Deletion of a domain

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")

The only new features here are the use of the HTTP DELETE method which, as its name suggests, is used to destroy the object concerned, and the fact that the identifier of this object, the domain name, is in the URI.

Adding a name server to all the domains

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)

We recover the list of our domains, as before, using the GET method. The name server to be added is indicated on the command line. This list of domains is placed in the responses variable, to which we will then apply, with Enum.map, a function to update the domain, using the HTTP PATCH method5.

Note that, this time, the processing of the error is more detailed: we react to the HTTP response status code 404 with a better message. For the 400, indicating an invalid request, we decode the JSON sent to be able to indicate an information message.


1 – As with any program, whatever the programming language used and whatever the licence – free or proprietary- of the external software components used, we have to take account of the risks in the software supply chain. Ideally the external modules must be loaded only from a trusted source, and no source should be trusted blindly.

2 – post! (with the exclamation mark at the end) raises an exception in the event of a problem, instead of returning the atom :error. The same applies to the other functions with this exclamation mark.

3 – We don’t use JSON in the request but the usual encoding of the web forms; hence the atom :form at the beginning.

4 – If we want more readable error messages, for example for end users, we can recover the body of the response from JSON, which contains information on what didn’t go right. For example, in the case of a wrong password, we will recover {“error”:”invalid_grant”,”error_description”:”Invalid user credentials”}.

5 – Normalised at source in RFC 5789.