Current section
Files
Jump to
Current section
Files
lib/digex_request.ex
defmodule DigexRequest do
@moduledoc "README.md" |> File.read!()
@type method :: :get | :head | :post | :delete | :put | :patch | :options | :trace
@type headers() :: [{header_name :: String.t(), header_value :: String.t()}]
@type options :: Keyword.t()
@type t :: %__MODULE__{
method: method(),
url: String.t(),
body: iodata(),
username: String.t(),
password: String.t(),
headers: headers(),
authorization: DigexRequest.Authorization.t() | nil
}
defstruct method: :get,
url: nil,
body: nil,
headers: [],
username: nil,
password: nil,
authorization: nil
@doc """
Send the query using an http client and map the response to `DigexRequest.Reponse` struct.
The default implementation use the `httpc` http client to send requests.
"""
@callback handle_request(method(), String.t(), headers(), any(), options()) ::
{:ok, DigexRequest.Response.t()} | {:error, any()}
@optional_callbacks handle_request: 5
defmacro __using__(_opts) do
quote do
alias DigexRequest.{Authorization, WWWAuthenticate}
@behaviour DigexRequest
@spec request(DigexRequest.t(), DigexRequest.options()) ::
{:ok, DigexRequest.Response.t(), DigexRequest.t()} | {:error, term()}
def request(%DigexRequest{} = req, opts \\ []) do
with {:ok, %{status: 401} = resp, req} <- do_request(req, opts) do
case www_digest_header(resp.headers) do
[] ->
{:ok, resp, req}
[header | _tail] ->
do_request(
%DigexRequest{
req
| authorization: build_authorization_from_header(header)
},
opts
)
end
end
end
def handle_request(method, url, headers, body, opts) do
Application.ensure_started(:inets)
headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end)
request =
if method == :get, do: {url, headers}, else: {url, headers, '', to_charlist(body)}
with {:ok, {status, headers, body}} <- :httpc.request(method, request, [], []) do
headers = Enum.map(headers, fn {k, v} -> {to_string(k), to_string(v)} end)
{:ok,
%DigexRequest.Response{
status: elem(status, 1),
headers: headers,
body: to_string(body)
}}
end
end
defp do_request(%DigexRequest{} = req, opts) do
req = build_headers(req)
case handle_request(req.method, req.url, req.headers, req.body, opts) do
{:ok, resp} -> {:ok, resp, req}
error -> error
end
end
defp build_headers(%DigexRequest{authorization: nil} = req), do: req
defp build_headers(%DigexRequest{authorization: auth} = req) do
auth = Authorization.refresh(auth, req)
headers = delete_authorization_header(req.headers)
%DigexRequest{
req
| authorization: auth,
headers: [{"authorization", Authorization.build(auth)} | headers]
}
end
defp www_digest_header(headers) do
headers
|> Enum.filter(fn {key, value} ->
key == "www-authenticate" and String.starts_with?(value, "Digest")
end)
|> Enum.map(fn {_, value} -> value end)
end
defp build_authorization_from_header(header) do
header
|> WWWAuthenticate.parse()
|> Authorization.new()
end
defp delete_authorization_header(headers) do
Enum.filter(headers, fn
{"authorization", _} -> false
{_, _} -> true
end)
end
defoverridable handle_request: 5
end
end
@doc """
Build a digest request to be sent
"""
@spec new(method(), String.t(), String.t(), String.t(), headers(), iodata() | nil) ::
DigexRequest.t()
def new(method, url, username, password, headers \\ [], body \\ nil) do
%__MODULE__{
method: method,
url: url,
headers: headers,
body: body,
username: username,
password: password
}
end
end