From 30d063949d6f329ad4b3fb6ab2854b1ae1fabb3e Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Mon, 27 Jan 2025 18:47:01 +1300 Subject: [PATCH 1/8] Start extracting device auth for an rpc experiment --- lib/nerves_hub/products.ex | 7 ++ lib/nerves_hub/rpc/device_auth.ex | 88 ++++++++++++++++++++ lib/nerves_hub_web/channels/device_socket.ex | 79 ++---------------- lib/nerves_hub_web/live/product/settings.ex | 3 +- 4 files changed, 101 insertions(+), 76 deletions(-) create mode 100644 lib/nerves_hub/rpc/device_auth.ex diff --git a/lib/nerves_hub/products.ex b/lib/nerves_hub/products.ex index d830f0d97..e5e48c8c2 100644 --- a/lib/nerves_hub/products.ex +++ b/lib/nerves_hub/products.ex @@ -313,4 +313,11 @@ defmodule NervesHub.Products do :nope end) end + + def shared_secrets_enabled?() do + # NervesHubWeb.DeviceSocket should really be :device_auth or :product_auth + Application.get_env(:nerves_hub, NervesHubWeb.DeviceSocket, []) + |> Keyword.get(:shared_secrets, []) + |> Keyword.get(:enabled, false) + end end diff --git a/lib/nerves_hub/rpc/device_auth.ex b/lib/nerves_hub/rpc/device_auth.ex new file mode 100644 index 000000000..a1e5c525a --- /dev/null +++ b/lib/nerves_hub/rpc/device_auth.ex @@ -0,0 +1,88 @@ +defmodule NervesHub.RPC.DeviceAuth do + @moduledoc false + + alias NervesHub.Devices + alias NervesHub.Products + + alias Plug.Crypto + + # Default 90 seconds max age for the signature + @default_max_hmac_age 90 + + def connect_device({:shared_secrets, x_headers}) do + headers = Map.new(x_headers) + + with :ok <- check_shared_secret_enabled(), + {:ok, key, salt, verification_opts} <- decode_from_headers(headers), + {:ok, auth} <- get_shared_secret_auth(key), + {:ok, signature} <- Map.fetch(headers, "x-nh-signature"), + {:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts), + {:ok, device} <- get_or_maybe_create_device(auth, identifier) do + {:ok, Devices.preload_product(device)} + else + error -> + :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ + auth: :shared_secrets, + reason: error, + product_key: Map.get(headers, "x-nh-key", "*empty*") + }) + + {:error, :invalid_auth} + end + end + + defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do + with [digest_str, iter_str, klen_str] <- String.split(alg, "-"), + digest <- String.to_existing_atom(String.downcase(digest_str)), + {iterations, ""} <- Integer.parse(iter_str), + {key_length, ""} <- Integer.parse(klen_str), + {signed_at, ""} <- Integer.parse(headers["x-nh-time"]), + {:ok, key} <- Map.fetch(headers, "x-nh-key") do + expected_salt = """ + NH1:device-socket:shared-secret:connect + + x-nh-alg=NH1-HMAC-#{alg} + x-nh-key=#{key} + x-nh-time=#{signed_at} + """ + + opts = [ + key_length: key_length, + key_iterations: iterations, + key_digest: digest, + signed_at: signed_at, + max_age: max_hmac_age() + ] + + {:ok, key, expected_salt, opts} + end + end + + defp decode_from_headers(_headers), do: {:error, :headers_decode_failed} + + defp get_shared_secret_auth("nhp_" <> _ = key), do: Products.get_shared_secret_auth(key) + defp get_shared_secret_auth(key), do: Devices.get_shared_secret_auth(key) + + defp get_or_maybe_create_device(%Products.SharedSecretAuth{} = auth, identifier) do + # TODO: Support JITP profile here to decide if enabled or what tags to use + Devices.get_or_create_device(auth, identifier) + end + + defp get_or_maybe_create_device(%{device: %{identifier: identifier} = device}, identifier), + do: {:ok, device} + + defp get_or_maybe_create_device(_auth, _identifier), do: {:error, :bad_identifier} + + defp max_hmac_age() do + Application.get_env(:nerves_hub, __MODULE__, []) + |> Keyword.get(:max_age, @default_max_hmac_age) + end + + defp check_shared_secret_enabled() do + if Products.shared_secrets_enabled?() do + :ok + else + {:error, :shared_secrets_not_enabled} + end + end +end diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index 0b0e8ca5a..51999a273 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -7,18 +7,14 @@ defmodule NervesHubWeb.DeviceSocket do alias NervesHub.Devices alias NervesHub.Devices.Connections alias NervesHub.Devices.DeviceConnection - alias NervesHub.Products alias NervesHub.Tracker - alias Plug.Crypto + alias NervesHub.RPC.DeviceAuth channel("console", NervesHubWeb.ConsoleChannel) channel("device", NervesHubWeb.DeviceChannel) channel("extensions", NervesHubWeb.ExtensionsChannel) - # Default 90 seconds max age for the signature - @default_max_hmac_age 90 - defoverridable init: 1, handle_in: 2, terminate: 2 @impl Phoenix.Socket.Transport @@ -96,14 +92,10 @@ defmodule NervesHubWeb.DeviceSocket do when is_list(x_headers) and length(x_headers) > 0 do headers = Map.new(x_headers) - with :ok <- check_shared_secret_enabled(), - {:ok, key, salt, verification_opts} <- decode_from_headers(headers), - {:ok, auth} <- get_shared_secret_auth(key), - {:ok, signature} <- Map.fetch(headers, "x-nh-signature"), - {:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts), - {:ok, device} <- get_or_maybe_create_device(auth, identifier) do - socket_and_assigns(socket, device) - else + case DeviceAuth.connect_device({:shared_secrets, x_headers}) do + {:ok, device} -> + socket_and_assigns(socket, Devices.preload_product(device)) + error -> :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ auth: :shared_secrets, @@ -133,61 +125,6 @@ defmodule NervesHubWeb.DeviceSocket do ] end - defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do - with [digest_str, iter_str, klen_str] <- String.split(alg, "-"), - digest <- String.to_existing_atom(String.downcase(digest_str)), - {iterations, ""} <- Integer.parse(iter_str), - {key_length, ""} <- Integer.parse(klen_str), - {signed_at, ""} <- Integer.parse(headers["x-nh-time"]), - {:ok, key} <- Map.fetch(headers, "x-nh-key") do - expected_salt = """ - NH1:device-socket:shared-secret:connect - - x-nh-alg=NH1-HMAC-#{alg} - x-nh-key=#{key} - x-nh-time=#{signed_at} - """ - - opts = [ - key_length: key_length, - key_iterations: iterations, - key_digest: digest, - signed_at: signed_at, - max_age: max_hmac_age() - ] - - {:ok, key, expected_salt, opts} - end - end - - defp decode_from_headers(_headers), do: {:error, :headers_decode_failed} - - defp get_shared_secret_auth("nhp_" <> _ = key), do: Products.get_shared_secret_auth(key) - defp get_shared_secret_auth(key), do: Devices.get_shared_secret_auth(key) - - defp get_or_maybe_create_device(%Products.SharedSecretAuth{} = auth, identifier) do - # TODO: Support JITP profile here to decide if enabled or what tags to use - Devices.get_or_create_device(auth, identifier) - end - - defp get_or_maybe_create_device(%{device: %{identifier: identifier} = device}, identifier), - do: {:ok, device} - - defp get_or_maybe_create_device(_auth, _identifier), do: {:error, :bad_identifier} - - defp max_hmac_age() do - Application.get_env(:nerves_hub, __MODULE__, []) - |> Keyword.get(:max_age, @default_max_hmac_age) - end - - defp check_shared_secret_enabled() do - if shared_secrets_enabled?() do - :ok - else - {:error, :shared_secrets_not_enabled} - end - end - defp socket_and_assigns(socket, device) do # disconnect devices using the same identifier _ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{}) @@ -273,10 +210,4 @@ defmodule NervesHubWeb.DeviceSocket do defp last_seen_update_interval() do Application.get_env(:nerves_hub, :device_last_seen_update_interval_minutes) end - - def shared_secrets_enabled?() do - Application.get_env(:nerves_hub, __MODULE__, []) - |> Keyword.get(:shared_secrets, []) - |> Keyword.get(:enabled, false) - end end diff --git a/lib/nerves_hub_web/live/product/settings.ex b/lib/nerves_hub_web/live/product/settings.ex index 7be6f909f..6890c401f 100644 --- a/lib/nerves_hub_web/live/product/settings.ex +++ b/lib/nerves_hub_web/live/product/settings.ex @@ -3,7 +3,6 @@ defmodule NervesHubWeb.Live.Product.Settings do alias NervesHub.Extensions alias NervesHub.Products - alias NervesHubWeb.DeviceSocket def mount(_params, _session, socket) do product = Products.load_shared_secret_auth(socket.assigns.product) @@ -14,7 +13,7 @@ defmodule NervesHubWeb.Live.Product.Settings do |> sidebar_tab(:settings) |> assign(:product, product) |> assign(:shared_secrets, product.shared_secret_auths) - |> assign(:shared_auth_enabled, DeviceSocket.shared_secrets_enabled?()) + |> assign(:shared_auth_enabled, Products.shared_secrets_enabled?()) |> assign(:form, to_form(Ecto.Changeset.change(product))) |> assign(:available_extensions, extensions()) From d7c0dbd5fb3b5f64db1d601bb1ba6957ab1695e6 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Mon, 27 Jan 2025 20:00:12 +1300 Subject: [PATCH 2/8] Add support for cert auth, including `on_connect` logic --- lib/nerves_hub/devices.ex | 13 ++++ lib/nerves_hub/rpc/device_auth.ex | 51 +++++++++++++- lib/nerves_hub_web/channels/device_socket.ex | 72 +++----------------- 3 files changed, 73 insertions(+), 63 deletions(-) diff --git a/lib/nerves_hub/devices.ex b/lib/nerves_hub/devices.ex index 26918baa3..a0fc10705 100644 --- a/lib/nerves_hub/devices.ex +++ b/lib/nerves_hub/devices.ex @@ -101,6 +101,19 @@ defmodule NervesHub.Devices do |> Repo.one!() end + def get_device_by_x509(cert) do + fingerprint = NervesHub.Certificate.fingerprint(cert) + + Device + |> join(:inner, [d], dc in assoc(d, :device_certificates)) + |> where([d, dc], dc.fingerprint == ^fingerprint) + |> Repo.one() + |> case do + nil -> {:error, :not_found} + certificate -> {:ok, certificate} + end + end + @spec filter(Product.t(), map()) :: %{ entries: list(Device.t()), current_page: non_neg_integer(), diff --git a/lib/nerves_hub/rpc/device_auth.ex b/lib/nerves_hub/rpc/device_auth.ex index a1e5c525a..317af56a2 100644 --- a/lib/nerves_hub/rpc/device_auth.ex +++ b/lib/nerves_hub/rpc/device_auth.ex @@ -2,13 +2,34 @@ defmodule NervesHub.RPC.DeviceAuth do @moduledoc false alias NervesHub.Devices + alias NervesHub.Devices.Connections + alias NervesHub.Devices.DeviceConnection + alias NervesHub.Devices.Device alias NervesHub.Products + alias NervesHub.Tracker alias Plug.Crypto # Default 90 seconds max age for the signature @default_max_hmac_age 90 + def connect_device({:ssl_certs, ssl_cert}) do + X509.Certificate.from_der!(ssl_cert) + |> Devices.get_device_by_x509() + |> case do + {:ok, device} -> + {:ok, on_connect(device)} + + error -> + :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ + auth: :cert, + reason: error + }) + + {:error, :invalid_auth} + end + end + def connect_device({:shared_secrets, x_headers}) do headers = Map.new(x_headers) @@ -18,7 +39,7 @@ defmodule NervesHub.RPC.DeviceAuth do {:ok, signature} <- Map.fetch(headers, "x-nh-signature"), {:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts), {:ok, device} <- get_or_maybe_create_device(auth, identifier) do - {:ok, Devices.preload_product(device)} + {:ok, on_connect(device)} else error -> :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ @@ -31,6 +52,34 @@ defmodule NervesHub.RPC.DeviceAuth do end end + defp on_connect(%Device{status: :registered} = device) do + Devices.set_as_provisioned!(device) + |> on_connect() + end + + defp on_connect(device) do + # disconnect devices using the same identifier + Phoenix.Channel.Server.broadcast_from!( + NervesHub.PubSub, + self(), + "device_socket:#{device.id}", + "disconnect", + %{} + ) + + {:ok, %DeviceConnection{id: connection_id}} = Connections.device_connected(device.id) + + :telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{ + ref_id: connection_id, + identifier: device.identifier, + firmware_uuid: get_in(device, [Access.key(:firmware_metadata), Access.key(:uuid)]) + }) + + Tracker.online(device) + + {connection_id, Devices.preload_product(device)} + end + defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do with [digest_str, iter_str, klen_str] <- String.split(alg, "-"), digest <- String.to_existing_atom(String.downcase(digest_str)), diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index 51999a273..e82bbb6ed 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -4,9 +4,8 @@ defmodule NervesHubWeb.DeviceSocket do require Logger - alias NervesHub.Devices alias NervesHub.Devices.Connections - alias NervesHub.Devices.DeviceConnection + alias NervesHub.Tracker alias NervesHub.RPC.DeviceAuth @@ -17,13 +16,6 @@ defmodule NervesHubWeb.DeviceSocket do defoverridable init: 1, handle_in: 2, terminate: 2 - @impl Phoenix.Socket.Transport - def init(state_tuple) do - {:ok, {state, socket}} = super(state_tuple) - socket = on_connect(socket) - {:ok, {state, socket}} - end - @impl Phoenix.Socket.Transport @decorate with_span("Channels.DeviceSocket.terminate") def terminate(reason, {_channels_info, socket} = state) do @@ -70,19 +62,12 @@ defmodule NervesHubWeb.DeviceSocket do @decorate with_span("Channels.DeviceSocket.connect") def connect(_params, socket, %{peer_data: %{ssl_cert: ssl_cert}}) when not is_nil(ssl_cert) do - X509.Certificate.from_der!(ssl_cert) - |> Devices.get_device_by_x509() - |> case do - {:ok, device} -> - socket_and_assigns(socket, device) + case DeviceAuth.connect_device({:ssl_certs, ssl_cert}) do + {:ok, ref_and_device} -> + socket_and_assigns(socket, ref_and_device) error -> - :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ - auth: :cert, - reason: error - }) - - {:error, :invalid_auth} + error end end @@ -90,20 +75,12 @@ defmodule NervesHubWeb.DeviceSocket do @decorate with_span("Channels.DeviceSocket.connect") def connect(_params, socket, %{x_headers: x_headers}) when is_list(x_headers) and length(x_headers) > 0 do - headers = Map.new(x_headers) - case DeviceAuth.connect_device({:shared_secrets, x_headers}) do - {:ok, device} -> - socket_and_assigns(socket, Devices.preload_product(device)) + {:ok, ref_and_device} -> + socket_and_assigns(socket, ref_and_device) error -> - :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ - auth: :shared_secrets, - reason: error, - product_key: Map.get(headers, "x-nh-key", "*empty*") - }) - - {:error, :invalid_auth} + error end end @@ -125,44 +102,15 @@ defmodule NervesHubWeb.DeviceSocket do ] end - defp socket_and_assigns(socket, device) do - # disconnect devices using the same identifier - _ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{}) - + defp socket_and_assigns(socket, {ref_id, device}) do socket = socket |> assign(:device, device) + |> assign(:reference_id, ref_id) {:ok, socket} end - @decorate with_span("Channels.DeviceSocket.on_connect#registered") - defp on_connect(%{assigns: %{device: %{status: :registered} = device}} = socket) do - socket - |> assign(device: Devices.set_as_provisioned!(device)) - |> on_connect() - end - - @decorate with_span("Channels.DeviceSocket.on_connect#provisioned") - defp on_connect(%{assigns: %{device: device}} = socket) do - # Report connection and use connection id as reference - {:ok, %DeviceConnection{id: connection_id}} = - Connections.device_connected(device.id) - - :telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{ - ref_id: connection_id, - identifier: socket.assigns.device.identifier, - firmware_uuid: - get_in(socket.assigns.device, [Access.key(:firmware_metadata), Access.key(:uuid)]) - }) - - Tracker.online(device) - - socket - |> assign(:device, device) - |> assign(:reference_id, connection_id) - end - @decorate with_span("Channels.DeviceSocket.on_disconnect") defp on_disconnect(exit_reason, socket) From f7118b2604401f22c8a3a1bc6eaae0ef6db71dd1 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Mon, 27 Jan 2025 20:45:55 +1300 Subject: [PATCH 3/8] Add support for when a device disconnects --- lib/nerves_hub/rpc/device_auth.ex | 22 ++++++++++++++++++++ lib/nerves_hub_web/channels/device_socket.ex | 7 ++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/nerves_hub/rpc/device_auth.ex b/lib/nerves_hub/rpc/device_auth.ex index 317af56a2..5fb8861a7 100644 --- a/lib/nerves_hub/rpc/device_auth.ex +++ b/lib/nerves_hub/rpc/device_auth.ex @@ -52,6 +52,28 @@ defmodule NervesHub.RPC.DeviceAuth do end end + def disconnect_device({:error, {:shutdown, :disconnected}}, device, reference_id) do + :telemetry.execute([:nerves_hub, :devices, :duplicate_connection], %{count: 1}, %{ + ref_id: reference_id, + device: device + }) + + disconnect_device(:ok, device, reference_id) + end + + def disconnect_device(_, device, reference_id) do + :telemetry.execute([:nerves_hub, :devices, :disconnect], %{count: 1}, %{ + ref_id: reference_id, + identifier: device.identifier + }) + + {:ok, _device_connection} = Connections.device_disconnected(reference_id) + + Tracker.offline(device) + + :ok + end + defp on_connect(%Device{status: :registered} = device) do Devices.set_as_provisioned!(device) |> on_connect() diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index e82bbb6ed..d1d5e49b0 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -6,8 +6,6 @@ defmodule NervesHubWeb.DeviceSocket do alias NervesHub.Devices.Connections - alias NervesHub.Tracker - alias NervesHub.RPC.DeviceAuth channel("console", NervesHubWeb.ConsoleChannel) @@ -19,7 +17,8 @@ defmodule NervesHubWeb.DeviceSocket do @impl Phoenix.Socket.Transport @decorate with_span("Channels.DeviceSocket.terminate") def terminate(reason, {_channels_info, socket} = state) do - on_disconnect(reason, socket) + %{assigns: %{device: device, reference_id: reference_id}} = socket + DeviceAuth.disconnect_device(reason, device, reference_id) super(reason, state) end @@ -114,6 +113,7 @@ defmodule NervesHubWeb.DeviceSocket do @decorate with_span("Channels.DeviceSocket.on_disconnect") defp on_disconnect(exit_reason, socket) + @decorate with_span("Channels.DeviceSocket.on_disconnect") defp on_disconnect({:error, reason}, %{ assigns: %{ device: device, @@ -132,6 +132,7 @@ defmodule NervesHubWeb.DeviceSocket do :ok end + @decorate with_span("Channels.DeviceSocket.on_disconnect") defp on_disconnect(_, %{ assigns: %{ device: device, From cbf34d6f2bc1d1da39a4ac48b77517535595ebf8 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 29 Jan 2025 15:53:51 +1300 Subject: [PATCH 4/8] Use `DeviceLink` for module namespacing And add some specs and docs --- .../connections.ex} | 26 +++++++++++++++---- lib/nerves_hub_web/channels/device_socket.ex | 16 ++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) rename lib/nerves_hub/{rpc/device_auth.ex => device_link/connections.ex} (83%) diff --git a/lib/nerves_hub/rpc/device_auth.ex b/lib/nerves_hub/device_link/connections.ex similarity index 83% rename from lib/nerves_hub/rpc/device_auth.ex rename to lib/nerves_hub/device_link/connections.ex index 5fb8861a7..5373d9faa 100644 --- a/lib/nerves_hub/rpc/device_auth.ex +++ b/lib/nerves_hub/device_link/connections.ex @@ -1,8 +1,10 @@ -defmodule NervesHub.RPC.DeviceAuth do - @moduledoc false +defmodule NervesHub.DeviceLink.Connections do + @moduledoc """ + Functions for connecting devices, including the reporting of connection availability. + """ alias NervesHub.Devices - alias NervesHub.Devices.Connections + alias NervesHub.Devices.Connections, as: DeviceConnections alias NervesHub.Devices.DeviceConnection alias NervesHub.Devices.Device alias NervesHub.Products @@ -13,6 +15,12 @@ defmodule NervesHub.RPC.DeviceAuth do # Default 90 seconds max age for the signature @default_max_hmac_age 90 + @type auth() :: {:ssl_certs, any()} | {:shared_secrets, list()} + @type connection_id() :: binary() + + @spec connect_device(auth()) :: {:ok, {connection_id(), Device.t()}} | {:error, :invalid_auth} + def connect_device(auth) + def connect_device({:ssl_certs, ssl_cert}) do X509.Certificate.from_der!(ssl_cert) |> Devices.get_device_by_x509() @@ -52,6 +60,9 @@ defmodule NervesHub.RPC.DeviceAuth do end end + @spec disconnect_device(any(), Device.t(), connection_id()) :: :ok + def disconnect_device(reason, device, reference_id) + def disconnect_device({:error, {:shutdown, :disconnected}}, device, reference_id) do :telemetry.execute([:nerves_hub, :devices, :duplicate_connection], %{count: 1}, %{ ref_id: reference_id, @@ -67,13 +78,18 @@ defmodule NervesHub.RPC.DeviceAuth do identifier: device.identifier }) - {:ok, _device_connection} = Connections.device_disconnected(reference_id) + {:ok, _device_connection} = DeviceConnections.device_disconnected(reference_id) Tracker.offline(device) :ok end + @spec device_heartbeat(connection_id()) :: :ok + def device_heartbeat(reference_id) do + DeviceConnections.device_heartbeat(reference_id) + end + defp on_connect(%Device{status: :registered} = device) do Devices.set_as_provisioned!(device) |> on_connect() @@ -89,7 +105,7 @@ defmodule NervesHub.RPC.DeviceAuth do %{} ) - {:ok, %DeviceConnection{id: connection_id}} = Connections.device_connected(device.id) + {:ok, %DeviceConnection{id: connection_id}} = DeviceConnections.device_connected(device.id) :telemetry.execute([:nerves_hub, :devices, :connect], %{count: 1}, %{ ref_id: connection_id, diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index d1d5e49b0..6d3a83096 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -4,9 +4,7 @@ defmodule NervesHubWeb.DeviceSocket do require Logger - alias NervesHub.Devices.Connections - - alias NervesHub.RPC.DeviceAuth + alias NervesHub.DeviceLink.Connections channel("console", NervesHubWeb.ConsoleChannel) channel("device", NervesHubWeb.DeviceChannel) @@ -18,7 +16,7 @@ defmodule NervesHubWeb.DeviceSocket do @decorate with_span("Channels.DeviceSocket.terminate") def terminate(reason, {_channels_info, socket} = state) do %{assigns: %{device: device, reference_id: reference_id}} = socket - DeviceAuth.disconnect_device(reason, device, reference_id) + Connections.disconnect_device(reason, device, reference_id) super(reason, state) end @@ -58,10 +56,10 @@ defmodule NervesHubWeb.DeviceSocket do # Used by Devices connecting with SSL certificates @impl Phoenix.Socket - @decorate with_span("Channels.DeviceSocket.connect") + @decorate with_span("Channels.DeviceSocket.connect#ssl_cert") def connect(_params, socket, %{peer_data: %{ssl_cert: ssl_cert}}) when not is_nil(ssl_cert) do - case DeviceAuth.connect_device({:ssl_certs, ssl_cert}) do + case Connections.connect_device({:ssl_certs, ssl_cert}) do {:ok, ref_and_device} -> socket_and_assigns(socket, ref_and_device) @@ -71,10 +69,11 @@ defmodule NervesHubWeb.DeviceSocket do end # Used by Devices connecting with HMAC Shared Secrets - @decorate with_span("Channels.DeviceSocket.connect") + @impl Phoenix.Socket + @decorate with_span("Channels.DeviceSocket.connect#shared_secrets") def connect(_params, socket, %{x_headers: x_headers}) when is_list(x_headers) and length(x_headers) > 0 do - case DeviceAuth.connect_device({:shared_secrets, x_headers}) do + case Connections.connect_device({:shared_secrets, x_headers}) do {:ok, ref_and_device} -> socket_and_assigns(socket, ref_and_device) @@ -83,6 +82,7 @@ defmodule NervesHubWeb.DeviceSocket do end end + @impl Phoenix.Socket def connect(_params, _socket, _connect_info) do {:error, :no_auth} end From 959aaa3e8b61ee076adbce2129cfdad128e43798 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 29 Jan 2025 16:00:17 +1300 Subject: [PATCH 5/8] The `preload_product` isn't needed --- lib/nerves_hub/device_link/connections.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nerves_hub/device_link/connections.ex b/lib/nerves_hub/device_link/connections.ex index 5373d9faa..294d90354 100644 --- a/lib/nerves_hub/device_link/connections.ex +++ b/lib/nerves_hub/device_link/connections.ex @@ -115,7 +115,7 @@ defmodule NervesHub.DeviceLink.Connections do Tracker.online(device) - {connection_id, Devices.preload_product(device)} + {connection_id, device} end defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do From dfe81ce38a9df6bcda0c0bbd697d196f4a699528 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 29 Jan 2025 16:00:38 +1300 Subject: [PATCH 6/8] Remove a duplicate function --- lib/nerves_hub/devices.ex | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/lib/nerves_hub/devices.ex b/lib/nerves_hub/devices.ex index a0fc10705..26918baa3 100644 --- a/lib/nerves_hub/devices.ex +++ b/lib/nerves_hub/devices.ex @@ -101,19 +101,6 @@ defmodule NervesHub.Devices do |> Repo.one!() end - def get_device_by_x509(cert) do - fingerprint = NervesHub.Certificate.fingerprint(cert) - - Device - |> join(:inner, [d], dc in assoc(d, :device_certificates)) - |> where([d, dc], dc.fingerprint == ^fingerprint) - |> Repo.one() - |> case do - nil -> {:error, :not_found} - certificate -> {:ok, certificate} - end - end - @spec filter(Product.t(), map()) :: %{ entries: list(Device.t()), current_page: non_neg_integer(), From d220987133e0d04a56cc21ab46ef3df91565edb6 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Wed, 29 Jan 2025 16:21:20 +1300 Subject: [PATCH 7/8] Simple functions docs --- lib/nerves_hub/device_link/connections.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/nerves_hub/device_link/connections.ex b/lib/nerves_hub/device_link/connections.ex index 294d90354..cc728bc43 100644 --- a/lib/nerves_hub/device_link/connections.ex +++ b/lib/nerves_hub/device_link/connections.ex @@ -18,6 +18,9 @@ defmodule NervesHub.DeviceLink.Connections do @type auth() :: {:ssl_certs, any()} | {:shared_secrets, list()} @type connection_id() :: binary() + @doc """ + Find and connect a device. Devices can also be created, if the strategy supports it. + """ @spec connect_device(auth()) :: {:ok, {connection_id(), Device.t()}} | {:error, :invalid_auth} def connect_device(auth) @@ -60,6 +63,9 @@ defmodule NervesHub.DeviceLink.Connections do end end + @doc """ + Mark a device as disconnected. + """ @spec disconnect_device(any(), Device.t(), connection_id()) :: :ok def disconnect_device(reason, device, reference_id) @@ -85,6 +91,9 @@ defmodule NervesHub.DeviceLink.Connections do :ok end + @doc """ + Mark a device as still connected and alive. + """ @spec device_heartbeat(connection_id()) :: :ok def device_heartbeat(reference_id) do DeviceConnections.device_heartbeat(reference_id) From 5ea42e10f6520d7b1f205e0a4557dba1e4599b81 Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Thu, 30 Jan 2025 15:04:56 +1300 Subject: [PATCH 8/8] Fixes after a rebase --- lib/nerves_hub/device_link/connections.ex | 4 +- lib/nerves_hub_web/channels/device_socket.ex | 46 -------------------- 2 files changed, 1 insertion(+), 49 deletions(-) diff --git a/lib/nerves_hub/device_link/connections.ex b/lib/nerves_hub/device_link/connections.ex index cc728bc43..19f0f5bfb 100644 --- a/lib/nerves_hub/device_link/connections.ex +++ b/lib/nerves_hub/device_link/connections.ex @@ -84,11 +84,9 @@ defmodule NervesHub.DeviceLink.Connections do identifier: device.identifier }) - {:ok, _device_connection} = DeviceConnections.device_disconnected(reference_id) + _ = DeviceConnections.device_disconnected(reference_id) Tracker.offline(device) - - :ok end @doc """ diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index 6d3a83096..5c8d93df0 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -110,52 +110,6 @@ defmodule NervesHubWeb.DeviceSocket do {:ok, socket} end - @decorate with_span("Channels.DeviceSocket.on_disconnect") - defp on_disconnect(exit_reason, socket) - - @decorate with_span("Channels.DeviceSocket.on_disconnect") - defp on_disconnect({:error, reason}, %{ - assigns: %{ - device: device, - reference_id: reference_id - } - }) do - if reason == {:shutdown, :disconnected} do - :telemetry.execute([:nerves_hub, :devices, :duplicate_connection], %{count: 1}, %{ - ref_id: reference_id, - device: device - }) - end - - shutdown(device, reference_id) - - :ok - end - - @decorate with_span("Channels.DeviceSocket.on_disconnect") - defp on_disconnect(_, %{ - assigns: %{ - device: device, - reference_id: reference_id - } - }) do - shutdown(device, reference_id) - end - - @decorate with_span("Channels.DeviceSocket.shutdown") - defp shutdown(device, reference_id) do - :telemetry.execute([:nerves_hub, :devices, :disconnect], %{count: 1}, %{ - ref_id: reference_id, - identifier: device.identifier - }) - - :ok = Connections.device_disconnected(reference_id) - - Tracker.offline(device) - - :ok - end - defp last_seen_update_interval() do Application.get_env(:nerves_hub, :device_last_seen_update_interval_minutes) end