wayfarer/test/support/http_request.ex
James Harton a381ca4b34
All checks were successful
continuous-integration/drone/push Build is passing
feat: add proxying. (#7)
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>
2023-11-19 17:56:07 +13:00

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