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