lib/ankh/tls.ex

defmodule Ankh.TLS do
  @moduledoc "TLS transport module"

  require Logger

  alias Ankh.Transport

  @opaque t :: %__MODULE__{socket: :ssl.socket()}
  defstruct socket: nil

  defimpl Transport do
    alias Ankh.TLS

    @default_connect_options binary: true,
                             active: false,
                             secure_renegotiate: true,
                             cacertfile: CAStore.file_path()

    def new(%TLS{} = transport, socket), do: {:ok, %{transport | socket: socket}}

    def connect(%TLS{} = transport, %URI{host: host, port: port}, timeout, options \\ []) do
      hostname = String.to_charlist(host)
      options = Keyword.merge(options, @default_connect_options)

      with {:ok, socket} <- :ssl.connect(hostname, port, options, timeout),
           :ok <- :ssl.setopts(socket, active: :once) do
        {:ok, %{transport | socket: socket}}
      end
    end

    def accept(%TLS{socket: socket} = transport, options \\ []) do
      options = Keyword.merge(options, active: :once)

      with :ok <- :ssl.controlling_process(socket, self()),
           :ok <- :ssl.setopts(socket, options) do
        {:ok, %{transport | socket: socket}}
      end
    end

    def send(%TLS{socket: socket}, data), do: :ssl.send(socket, data)

    def recv(%TLS{socket: socket}, size, timeout), do: :ssl.recv(socket, size, timeout)

    def close(%TLS{socket: socket} = transport) do
      with :ok <- :ssl.close(socket), do: {:ok, %{transport | socket: nil}}
    end

    def handle_msg(_tls, {:ssl, socket, data}) do
      with :ok <- :ssl.setopts(socket, active: :once), do: {:ok, data}
    end

    def handle_msg(_tls, {:ssl_error, _socket, reason}), do: {:error, reason}
    def handle_msg(_tls, {:ssl_closed, _socket}), do: {:error, :closed}
    def handle_msg(_tls, msg), do: {:other, msg}

    def negotiated_protocol(%TLS{socket: socket}), do: :ssl.negotiated_protocol(socket)
  end
end