lib/nerves_hub_link/configurator.ex

defmodule NervesHubLink.Configurator do
  alias NervesHubLink.Backoff
  alias __MODULE__.{Config, Default}
  require Logger

  @device_api_version "1.0.0"
  @console_version "1.0.0"

  defmodule Config do
    defstruct device_api_host: "device.nerves-hub.org",
              device_api_port: 443,
              device_api_sni: "device.nerves-hub.org",
              fwup_public_keys: [],
              fwup_devpath: "/dev/mmcblk0",
              fwup_env: [],
              nerves_key: [],
              params: %{},
              remote_iex: false,
              socket: [],
              ssl: []

    @type t() :: %__MODULE__{
            device_api_host: String.t(),
            device_api_port: String.t(),
            device_api_sni: charlist(),
            fwup_public_keys: [binary()],
            fwup_devpath: Path.t(),
            fwup_env: [{String.t(), String.t()}],
            nerves_key: any(),
            params: map(),
            remote_iex: boolean,
            socket: any(),
            ssl: [:ssl.tls_client_option()]
          }
  end

  @callback build(%Config{}) :: Config.t()

  @fwup_devpath "nerves_fw_devpath"

  @spec build :: Config.t()
  def build() do
    Application.get_env(:nerves_hub_link, :configurator, fetch_default())
    |> do_build()
    |> add_socket_opts()
    |> add_fwup_public_keys()
  end

  defp add_socket_opts(config) do
    # PhoenixClient requires these SSL options be passed as
    # [transport_opts: [socket_opts: ssl]]. So for convenience,
    # we'll bundle it all here as expected without overriding
    # any other items that may have been provided in :socket or
    # :transport_opts keys previously.
    transport_opts = config.socket[:transport_opts] || []
    transport_opts = Keyword.put(transport_opts, :socket_opts, config.ssl)

    socket =
      config.socket
      |> Keyword.put(:transport_opts, transport_opts)
      |> Keyword.put_new_lazy(:reconnect_after_msec, fn ->
        # Default retry interval
        # 1 second minimum delay that doubles up to 60 seconds. Up to 50% of
        # the delay is added to introduce jitter into the retry attempts.
        Backoff.delay_list(1000, 60000, 0.50)
      end)

    %{config | socket: socket}
  end

  defp base_config() do
    base = struct(Config, Application.get_all_env(:nerves_hub_link))

    url = "wss://#{base.device_api_host}:#{base.device_api_port}/socket/websocket"

    socket = Keyword.put_new(base.socket, :url, url)

    ssl =
      base.ssl
      |> Keyword.put_new(:verify, :verify_peer)
      |> Keyword.put_new(:versions, [:"tlsv1.2"])
      |> Keyword.put_new(:server_name_indication, to_charlist(base.device_api_sni))

    fwup_devpath = Nerves.Runtime.KV.get(@fwup_devpath)

    params =
      Nerves.Runtime.KV.get_all_active()
      |> Map.put("fwup_version", fwup_version())
      |> Map.put("device_api_version", @device_api_version)
      |> Map.put("console_version", @console_version)

    %{base | params: params, socket: socket, ssl: ssl, fwup_devpath: fwup_devpath}
  end

  defp do_build(configurator) when is_atom(configurator) do
    base_config()
    |> configurator.build()
  end

  defp do_build({m, f, a}) when is_atom(m) and is_atom(f) and is_list(a) do
    apply(m, f, [base_config() | a])
  end

  defp do_build(configurator) do
    raise "[NervesHubLink] Bad Configurator - #{inspect(configurator)}"
  end

  defp fetch_default() do
    if Code.ensure_loaded?(NervesKey) do
      NervesHubLink.Configurator.NervesKey
    else
      Default
    end
  end

  defp fwup_version do
    {version_string, 0} = System.cmd("fwup", ["--version"])
    String.trim(version_string)
  end

  defp add_fwup_public_keys(config) do
    # NervesHubLink.Certificate.fwup_public_keys() is compiled into the module
    # This is a simple workaround to support changing hardcoded binary keys
    # in the config and being able to load without recompiling. However, it
    # is still suggested to recompile as well which is required for resolve
    # public keys referenced by an atom
    fwup_public_keys =
      for key <- NervesHubLink.Certificate.fwup_public_keys() ++ config.fwup_public_keys,
          is_binary(key),
          do: key

    if fwup_public_keys == [] do
      Logger.error("No fwup public keys were configured for nerves_hub_link.")
      Logger.error("This means that firmware signatures are not being checked.")
      Logger.error("nerves_hub_link will fail to apply firmware updates.")
    end

    %{config | fwup_public_keys: fwup_public_keys}
  end
end