diff --git a/config/runtime.exs b/config/runtime.exs index 05fad1a78..6875d6586 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -203,7 +203,9 @@ if config_env() == :prod do config :nerves_hub, NervesHubWeb.DeviceSocket, shared_secrets: [ enabled: System.get_env("DEVICE_SHARED_SECRETS_ENABLED", "false") == "true" - ] + ], + web_endpoint_supported: + System.get_env("DEVICE_SOCKET_WEB_ENDPOINT_SUPPORTED", "true") == "true" end ## diff --git a/lib/nerves_hub_web/channels/device_socket.ex b/lib/nerves_hub_web/channels/device_socket.ex index 0b0e8ca5a..3e49fe9a0 100644 --- a/lib/nerves_hub_web/channels/device_socket.ex +++ b/lib/nerves_hub_web/channels/device_socket.ex @@ -92,11 +92,12 @@ defmodule NervesHubWeb.DeviceSocket do # Used by Devices connecting with HMAC Shared Secrets @decorate with_span("Channels.DeviceSocket.connect") - def connect(_params, socket, %{x_headers: x_headers}) + def connect(_params, socket, %{x_headers: x_headers, source: source}) when is_list(x_headers) and length(x_headers) > 0 do headers = Map.new(x_headers) - with :ok <- check_shared_secret_enabled(), + with :ok <- check_source_enabled(source), + :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"), @@ -104,6 +105,15 @@ defmodule NervesHubWeb.DeviceSocket do {:ok, device} <- get_or_maybe_create_device(auth, identifier) do socket_and_assigns(socket, device) else + {:error, :check_uri} = 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 + error -> :telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{ auth: :shared_secrets, @@ -188,6 +198,14 @@ defmodule NervesHubWeb.DeviceSocket do end end + defp check_source_enabled(source) do + if source_enabled?(source) do + :ok + else + {:error, :check_uri} + 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", %{}) @@ -279,4 +297,13 @@ defmodule NervesHubWeb.DeviceSocket do |> Keyword.get(:shared_secrets, []) |> Keyword.get(:enabled, false) end + + def source_enabled?(NervesHubWeb.DeviceEndpoint) do + true + end + + def source_enabled?(NervesHubWeb.Endpoint) do + Application.get_env(:nerves_hub, __MODULE__, []) + |> Keyword.get(:web_endpoint_supported, true) + end end diff --git a/lib/nerves_hub_web/device_endpoint.ex b/lib/nerves_hub_web/device_endpoint.ex index 0768153ad..4fd6fa46e 100644 --- a/lib/nerves_hub_web/device_endpoint.ex +++ b/lib/nerves_hub_web/device_endpoint.ex @@ -11,7 +11,7 @@ defmodule NervesHubWeb.DeviceEndpoint do "/socket", NervesHubWeb.DeviceSocket, websocket: [ - connect_info: [:peer_data, :x_headers], + connect_info: [:peer_data, :x_headers, source: __MODULE__], compress: true, timeout: 180_000, fullsweep_after: 0, diff --git a/lib/nerves_hub_web/endpoint.ex b/lib/nerves_hub_web/endpoint.ex index 199110b58..2f7582949 100644 --- a/lib/nerves_hub_web/endpoint.ex +++ b/lib/nerves_hub_web/endpoint.ex @@ -24,7 +24,7 @@ defmodule NervesHubWeb.Endpoint do "/device-socket", NervesHubWeb.DeviceSocket, websocket: [ - connect_info: [:peer_data, :x_headers], + connect_info: [:peer_data, :x_headers, source: __MODULE__], compress: true, timeout: 180_000, fullsweep_after: 0, diff --git a/lib/nerves_hub_web/helpers/websocket_connection_error.ex b/lib/nerves_hub_web/helpers/websocket_connection_error.ex index d957500f1..0fc02e05a 100644 --- a/lib/nerves_hub_web/helpers/websocket_connection_error.ex +++ b/lib/nerves_hub_web/helpers/websocket_connection_error.ex @@ -1,12 +1,19 @@ defmodule NervesHub.Helpers.WebsocketConnectionError do import Plug.Conn - @message "no certificate pair or shared secrets connection settings were provided" + @no_auth_message "no certificate pair or shared secrets connection settings were provided" + @check_uri_message "incorrect uri used, please contact support" def handle_error(conn, :no_auth) do conn - |> put_resp_header("nh-connection-error-reason", @message) - |> send_resp(401, @message) + |> put_resp_header("nh-connection-error-reason", @no_auth_message) + |> send_resp(401, @no_auth_message) + end + + def handle_error(conn, :check_uri) do + conn + |> put_resp_header("nh-connection-error-reason", @check_uri_message) + |> send_resp(404, @check_uri_message) end def handle_error(conn, _reason), do: send_resp(conn, 401, "") diff --git a/test/nerves_hub_web/channels/websocket_test.exs b/test/nerves_hub_web/channels/websocket_test.exs index fcd941723..343f050df 100644 --- a/test/nerves_hub_web/channels/websocket_test.exs +++ b/test/nerves_hub_web/channels/websocket_test.exs @@ -470,6 +470,49 @@ defmodule NervesHubWeb.WebsocketTest do end end + describe "web endpoint device connections" do + @describetag :tmp_dir + + setup do + Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, shared_secrets: [enabled: true]) + Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, web_endpoint_supported: false) + + on_exit(fn -> + Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, + shared_secrets: [enabled: false] + ) + + Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, web_endpoint_supported: true) + end) + end + + test "can disable devices connecting via the web endpoint", %{user: user} do + org = Fixtures.org_fixture(user) + product = Fixtures.product_fixture(user, org) + assert {:ok, auth} = Products.create_shared_secret_auth(product) + + identifier = Ecto.UUID.generate() + refute Repo.get_by(Device, identifier: identifier) + + opts = [ + mint_opts: [protocols: [:http1]], + uri: "ws://127.0.0.1:#{@web_port}/device-socket/websocket", + headers: Utils.nh1_key_secret_headers(auth, identifier) + ] + + {:ok, socket} = SocketClient.start_link(opts) + + SocketClient.wait_connect(socket) + + refute SocketClient.connected?(socket) + + assigns = SocketClient.state(socket).assigns + + assert assigns.error_code == 404 + assert assigns.error_reason == "incorrect uri used, please contact support" + end + end + describe "duplicate connections using the same device id" do @describetag :tmp_dir diff --git a/test/support/socket_client.ex b/test/support/socket_client.ex index 1e92700d1..fcd4b5f70 100644 --- a/test/support/socket_client.ex +++ b/test/support/socket_client.ex @@ -291,7 +291,7 @@ defmodule SocketClient do @impl Slipstream def handle_disconnect( - {:error, {:upgrade_failure, %{reason: %{status_code: 401} = reason}}}, + {:error, {:upgrade_failure, %{reason: reason}}}, socket ) do socket =