Skip to main content

lib/http/transport/ssl.ex

defmodule HTTP.Transport.SSL do
  @moduledoc false

  @behaviour HTTP.Transport

  @impl true
  def connect(host, port, opts, timeout) do
    ssl_opts = ssl_options(host, Keyword.get(opts, :ssl, []))

    socket_opts =
      [
        :binary,
        packet: :raw,
        active: false
      ] ++ ssl_opts ++ Keyword.get(opts, :socket_opts, [])

    :ssl.connect(String.to_charlist(host), port, socket_opts, timeout)
  end

  @impl true
  def controlling_process(socket, pid), do: :ssl.controlling_process(socket, pid)

  @impl true
  def send(socket, iodata), do: :ssl.send(socket, iodata)

  @impl true
  def setopts(socket, opts), do: :ssl.setopts(socket, opts)

  @impl true
  def close(socket), do: :ssl.close(socket)

  @impl true
  def normalize_message({:ssl, socket, data}, socket), do: {:data, data}
  def normalize_message({:ssl_closed, socket}, socket), do: :closed
  def normalize_message({:ssl_error, socket, reason}, socket), do: {:error, reason}
  def normalize_message(_, _), do: :unknown

  defp default_ssl_options(host) do
    [
      verify: :verify_peer,
      cacerts: :public_key.cacerts_get(),
      server_name_indication: String.to_charlist(host),
      customize_hostname_check: [
        match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
      ],
      versions: [:"tlsv1.3", :"tlsv1.2"],
      depth: 4
    ]
  end

  defp ssl_options(host, user_ssl_opts) do
    user_ssl_opts = normalize_user_ssl_options(user_ssl_opts)

    host
    |> default_ssl_options()
    |> maybe_drop_default_cacerts(user_ssl_opts)
    |> Keyword.merge(user_ssl_opts)
  end

  defp normalize_user_ssl_options(user_ssl_opts) do
    Enum.map(user_ssl_opts, fn
      {key, value} when key in [:cacertfile, :certfile, :keyfile] and is_binary(value) ->
        {key, String.to_charlist(value)}

      option ->
        option
    end)
  end

  defp maybe_drop_default_cacerts(defaults, user_ssl_opts) do
    if Keyword.has_key?(user_ssl_opts, :cacertfile) or Keyword.has_key?(user_ssl_opts, :cacerts) do
      Keyword.delete(defaults, :cacerts)
    else
      defaults
    end
  end
end