lib/safeurl/dns_resolver.ex

defmodule SafeURL.DNSResolver do
  @moduledoc """
  In some cases you might want to use a custom strategy
  for DNS resolution. You can do so by passing your own
  implementation of `SafeURL.DNSResolver` in the global
  or local config.

  By default, the `DNS` package is used for resolution,
  but you can replace it with a wrapper that uses
  different configuration or a completely different
  implementation altogether.


  ## Use-cases

    * Using a specific DNS server
    * Avoiding network access in specific environments
    * Mocking DNS resolution in tests


  ## Usage

  Start by creating a module that implements the
  `DNSResolver` behaviour. Currently, this means adding
  only one `c:resolve/1` callback that takes a host and
  returns a list of resolved IPs.

  As an example, suppose you wanted to use
  [Cloudflare's DNS](https://1.1.1.1/dns/), you can do
  that by wrapping `DNS` with your own settings in a new
  module:

      defmodule CloudflareDNS do
        @behaviour SafeURL.DNSResolver

        @impl true
        def resolve(domain) do
          DNS.resolve(domain, :a, {"1.1.1.1", 53}, :udp)
        end
      end

  To use it, simply pass it in the global config:

      config :safeurl, dns_module: CloudflareDNS

  You can also directly set the `:dns_module` in method options:

      SafeURL.allowed?("https://example.com", dns_module: CloudflareDNS)


  ## Testing

  This is especially useful in tests where you want to
  ensure your HTTP Client wrapper with `SafeURL` is
  working as expected.

  You can override the `:dns_module` config to ensure
  a specific IP is resolved for a domain or no network
  requests are made:

      defmodule TestDNSResolver do
        @behaviour SafeURL.DNSResolver

        @impl true
        def resolve("google.com"), do: {:ok, [{192, 168, 1, 10}]}
        def resolve("github.com"), do: {:ok, [{192, 168, 1, 20}]}
        def resolve(_domain),      do: {:ok, [{192, 168, 1, 99}]}
      end

  """

  @type resolution :: :inet.ip() | [:inet.ip()]
  @callback resolve(host :: binary()) :: {:ok, resolution()} | {:error, atom()}
end