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