James Harton
a381ca4b34
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: https://code.harton.nz/james/wayfarer/pulls/7 Co-authored-by: James Harton <james@harton.nz> Co-committed-by: James Harton <james@harton.nz>
97 lines
2.9 KiB
Elixir
97 lines
2.9 KiB
Elixir
defmodule Support.HttpRequest do
|
|
@moduledoc false
|
|
alias Mint.HTTP
|
|
import Wayfarer.Utils
|
|
|
|
defmacro __using__(_) do
|
|
quote do
|
|
import Support.HttpRequest
|
|
import IP.Sigil
|
|
end
|
|
end
|
|
|
|
@type headers :: [{String.t(), String.t()}]
|
|
|
|
@type options :: [
|
|
host: String.t(),
|
|
method: String.t(),
|
|
path: String.t(),
|
|
headers: headers,
|
|
body: iodata,
|
|
options: Keyword.t()
|
|
]
|
|
|
|
@doc """
|
|
Perform an HTTP request.
|
|
|
|
This is a little more annoying than most of the Elixir HTTP clients (Tesla,
|
|
Finch, etc) can handle easily because you need to make a potentially SNI SSL
|
|
request to an arbitrary address without resolving it via DNS.
|
|
"""
|
|
@spec request(
|
|
:http | :https,
|
|
:inet.ip_address() | String.t() | IP.Address.t(),
|
|
:inet.port_number(),
|
|
options
|
|
) ::
|
|
{:ok, %{status: nil | non_neg_integer(), headers: headers, body: iodata()}}
|
|
| {:error, any}
|
|
def request(scheme, address, port, options \\ []) do
|
|
host = Keyword.get(options, :host, "example.com")
|
|
method = Keyword.get(options, :method, "GET")
|
|
path = Keyword.get(options, :path, "/")
|
|
headers = Keyword.get(options, :headers, [])
|
|
body = Keyword.get(options, :body, [])
|
|
|
|
options =
|
|
options
|
|
|> Keyword.get(:options, [])
|
|
|> Keyword.put_new(:hostname, host)
|
|
|
|
Task.async(fn ->
|
|
with {:ok, address} <- sanitise_ip_address(address),
|
|
{:ok, mint} <- HTTP.connect(scheme, address, port, options),
|
|
{:ok, mint, req} <- HTTP.request(mint, method, path, headers, body) do
|
|
handle_response(mint, req, %{status: nil, headers: [], body: []})
|
|
end
|
|
end)
|
|
|> Task.await()
|
|
end
|
|
|
|
defp handle_response(mint, req, state) do
|
|
receive do
|
|
message ->
|
|
case HTTP.stream(mint, message) do
|
|
:unknown -> {:error, {:unknown_message, message}}
|
|
{:ok, mint, responses} -> handle_responses(responses, mint, req, state)
|
|
{:error, _, reason, _} -> {:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp handle_responses([], mint, req, state), do: handle_response(mint, req, state)
|
|
|
|
defp handle_responses([{:status, req, status} | responses], mint, req, state),
|
|
do: handle_responses(responses, mint, req, %{state | status: status})
|
|
|
|
defp handle_responses([{:headers, req, headers} | responses], mint, req, state),
|
|
do:
|
|
handle_responses(
|
|
responses,
|
|
mint,
|
|
req,
|
|
Map.update!(state, :headers, &Enum.concat(&1, headers))
|
|
)
|
|
|
|
defp handle_responses([{:data, req, body} | responses], mint, req, state),
|
|
do:
|
|
handle_responses(
|
|
responses,
|
|
mint,
|
|
req,
|
|
Map.update!(state, :body, &Enum.concat(&1, [body]))
|
|
)
|
|
|
|
defp handle_responses([{:done, req} | _], _mint, req, state), do: {:ok, state}
|
|
defp handle_responses([{:error, req, reason}], _mint, req, _state), do: {:error, reason}
|
|
end
|