98 lines
2.9 KiB
Elixir
98 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
|