lib/httpx/request.ex

defmodule HTTPX.Request do
  @moduledoc ~S"""
  A prepared HTTP request.

  Can be modified by processors and replayed multiple times.
  """

  @default_auth [
    basic: HTTPX.Auth.Basic
  ]
  @auth_methods Application.get_env(:httpx, :auth_extensions, []) ++ @default_auth
  @default_timeouts [
    connect_timeout: 5_000,
    recv_timeout: 15_000
  ]
  @default_ssl_options [ssl_options: [versions: [:"tlsv1.2"]]]

  if p = Application.get_env(:httpx, :default_pool, :default) do
    @default_settings [{:pool, p} | @default_timeouts ++ @default_ssl_options]
  else
    @default_settings @default_timeouts ++ @default_ssl_options
  end

  @ssl_verify Application.get_env(:httpx, :ssl_verify, true)
  @user_agent "HTTPX/#{Mix.Project.config()[:version]}"

  @doc false
  @spec __default_settings__ :: Keyword.t()
  def __default_settings__, do: @default_settings

  @typedoc @moduledoc
  @type t :: %__MODULE__{
          method: atom,
          url: String.t(),
          headers: [{String.t(), String.t()}],
          body: binary,
          format: atom,
          fail: boolean,
          settings: list,
          meta: map
        }

  defstruct [
    :method,
    :url,
    :headers,
    :body,
    :format,
    :fail,
    :settings,
    meta: %{}
  ]

  @doc ~S"""
  Prepare a request.
  """
  @spec prepare(atom, String.t(), Keyword.t()) :: t
  def prepare(method, url, options \\ []) do
    request = %__MODULE__{
      method: method,
      url: generate_url(url, options),
      headers: headers(options),
      body: options[:body] || "",
      settings: settings(options),
      format: options[:format] || :text,
      fail: options[:fail] || false
    }

    auth = options[:auth]

    if auth_method = @auth_methods[auth] || auth do
      auth_method.auth(request, options)
    else
      request
    end
  end

  ### Helpers ###

  @spec headers(Keyword.t()) :: [{String.t(), String.t()}]
  defp headers(options) do
    headers = options[:headers] || []

    if List.keymember?(headers, "user-agent", 0),
      do: headers,
      else: [{"user-agent", @user_agent} | headers]
  end

  @spec settings(Keyword.t()) :: list

  if @ssl_verify do
    defp settings(options) do
      settings = Keyword.merge(@default_settings, options[:settings] || [])

      settings =
        if ssl = settings[:ssl_options] do
          ssl = Keyword.merge(base_ssl(), ssl)
          Keyword.put(settings, :ssl_options, ssl)
        else
          settings
        end

      if options[:format] == :stream, do: settings, else: [:with_body | settings]
    end
  else
    defp settings(options) do
      settings = Keyword.merge(@default_settings, options[:settings] || [])
      if options[:format] == :stream, do: settings, else: [:with_body | settings]
    end
  end

  @spec generate_url(String.t(), Keyword.t()) :: String.t()
  defp generate_url(url, options) do
    uri = URI.parse(url)

    full_url =
      cond do
        not Keyword.has_key?(options, :params) ->
          url

        uri.query ->
          url <> "&" <> HTTPX.query_encode(options[:params] || %{})

        uri.path ->
          url <> "?" <> HTTPX.query_encode(options[:params] || %{})

        true ->
          url <> "/?" <> HTTPX.query_encode(options[:params] || %{})
      end

    full_url
    |> to_string
    |> default_process_url
  end

  @spec default_process_url(String.t()) :: String.t()
  defp default_process_url(url) do
    case url |> String.slice(0, 12) |> String.downcase() do
      "http://" <> _ -> url
      "https://" <> _ -> url
      "http+unix://" <> _ -> url
      _ -> "http://" <> url
    end
  end

  @compile {:inline, base_ssl: 0}
  @spec base_ssl :: Keyword.t()
  if :erlang.system_info(:otp_release)
     |> to_string
     |> String.split(".")
     |> List.first()
     |> String.to_integer()
     |> Kernel.>=(21) do
    defp base_ssl,
      do: [
        versions: [:"tlsv1.2"],
        verify: :verify_peer,
        cacertfile: :certifi.cacertfile(),
        depth: 99,
        # crl_check: :peer,
        crl_check: :best_effort,
        crl_cache: {:ssl_crl_cache, {:internal, [{:http, 5_000}]}},
        customize_hostname_check: [
          match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
        ]
      ]
  else
    defp base_ssl,
      do: [
        versions: [:"tlsv1.2"],
        verify: :verify_peer,
        cacertfile: :certifi.cacertfile(),
        depth: 99
      ]
  end
end