Skip to content

Commit 274bad9

Browse files
committed
Support restricting device connections to the web endpoint
1 parent 77535da commit 274bad9

File tree

6 files changed

+88
-8
lines changed

6 files changed

+88
-8
lines changed

config/runtime.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ if config_env() == :prod do
203203
config :nerves_hub, NervesHubWeb.DeviceSocket,
204204
shared_secrets: [
205205
enabled: System.get_env("DEVICE_SHARED_SECRETS_ENABLED", "false") == "true"
206-
]
206+
],
207+
web_endpoint_supported:
208+
System.get_env("DEVICE_SOCKET_WEB_ENDPOINT_SUPPORTED", "true") == "true"
207209
end
208210

209211
##

lib/nerves_hub_web/channels/device_socket.ex

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,28 @@ defmodule NervesHubWeb.DeviceSocket do
9292

9393
# Used by Devices connecting with HMAC Shared Secrets
9494
@decorate with_span("Channels.DeviceSocket.connect")
95-
def connect(_params, socket, %{x_headers: x_headers})
95+
def connect(_params, socket, %{x_headers: x_headers} = connect_info)
9696
when is_list(x_headers) and length(x_headers) > 0 do
9797
headers = Map.new(x_headers)
9898

99-
with :ok <- check_shared_secret_enabled(),
99+
with :ok <- check_source_enabled(connect_info[:source]),
100+
:ok <- check_shared_secret_enabled(),
100101
{:ok, key, salt, verification_opts} <- decode_from_headers(headers),
101102
{:ok, auth} <- get_shared_secret_auth(key),
102103
{:ok, signature} <- Map.fetch(headers, "x-nh-signature"),
103104
{:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts),
104105
{:ok, device} <- get_or_maybe_create_device(auth, identifier) do
105106
socket_and_assigns(socket, device)
106107
else
108+
{:error, :check_uri} = error ->
109+
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
110+
auth: :shared_secrets,
111+
reason: error,
112+
product_key: Map.get(headers, "x-nh-key", "*empty*")
113+
})
114+
115+
error
116+
107117
error ->
108118
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
109119
auth: :shared_secrets,
@@ -188,6 +198,14 @@ defmodule NervesHubWeb.DeviceSocket do
188198
end
189199
end
190200

201+
defp check_source_enabled(source) do
202+
if source_enabled?(source) do
203+
:ok
204+
else
205+
{:error, :check_uri}
206+
end
207+
end
208+
191209
defp socket_and_assigns(socket, device) do
192210
# disconnect devices using the same identifier
193211
_ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{})
@@ -279,4 +297,14 @@ defmodule NervesHubWeb.DeviceSocket do
279297
|> Keyword.get(:shared_secrets, [])
280298
|> Keyword.get(:enabled, false)
281299
end
300+
301+
def source_enabled?(nil) do
302+
true
303+
end
304+
305+
def source_enabled?(NervesHubWeb.Endpoint) do
306+
Application.get_env(:nerves_hub, __MODULE__, [])
307+
|> Keyword.get(:web_endpoint_supported, true)
308+
|> tap(fn v -> dbg(v) end)
309+
end
282310
end

lib/nerves_hub_web/endpoint.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ defmodule NervesHubWeb.Endpoint do
2424
"/device-socket",
2525
NervesHubWeb.DeviceSocket,
2626
websocket: [
27-
connect_info: [:peer_data, :x_headers],
27+
connect_info: [:peer_data, :x_headers, source: __MODULE__],
2828
compress: true,
2929
timeout: 180_000,
3030
fullsweep_after: 0,

lib/nerves_hub_web/helpers/websocket_connection_error.ex

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
defmodule NervesHub.Helpers.WebsocketConnectionError do
22
import Plug.Conn
33

4-
@message "no certificate pair or shared secrets connection settings were provided"
4+
@no_auth_message "no certificate pair or shared secrets connection settings were provided"
5+
@check_uri_message "incorrect uri used, please contact support"
56

67
def handle_error(conn, :no_auth) do
78
conn
8-
|> put_resp_header("nh-connection-error-reason", @message)
9-
|> send_resp(401, @message)
9+
|> put_resp_header("nh-connection-error-reason", @no_auth_message)
10+
|> send_resp(401, @no_auth_message)
11+
end
12+
13+
def handle_error(conn, :check_uri) do
14+
conn
15+
|> put_resp_header("nh-connection-error-reason", @check_uri_message)
16+
|> send_resp(404, @check_uri_message)
1017
end
1118

1219
def handle_error(conn, _reason), do: send_resp(conn, 401, "")

test/nerves_hub_web/channels/websocket_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,49 @@ defmodule NervesHubWeb.WebsocketTest do
470470
end
471471
end
472472

473+
describe "web endpoint device connections" do
474+
@describetag :tmp_dir
475+
476+
setup do
477+
Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, shared_secrets: [enabled: true])
478+
Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, web_endpoint_supported: false)
479+
480+
on_exit(fn ->
481+
Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket,
482+
shared_secrets: [enabled: false]
483+
)
484+
485+
Application.put_env(:nerves_hub, NervesHubWeb.DeviceSocket, web_endpoint_supported: true)
486+
end)
487+
end
488+
489+
test "can disable devices connecting via the web endpoint", %{user: user} do
490+
org = Fixtures.org_fixture(user)
491+
product = Fixtures.product_fixture(user, org)
492+
assert {:ok, auth} = Products.create_shared_secret_auth(product)
493+
494+
identifier = Ecto.UUID.generate()
495+
refute Repo.get_by(Device, identifier: identifier)
496+
497+
opts = [
498+
mint_opts: [protocols: [:http1]],
499+
uri: "ws://127.0.0.1:#{@web_port}/device-socket/websocket",
500+
headers: Utils.nh1_key_secret_headers(auth, identifier)
501+
]
502+
503+
{:ok, socket} = SocketClient.start_link(opts)
504+
505+
SocketClient.wait_connect(socket)
506+
507+
refute SocketClient.connected?(socket)
508+
509+
assigns = SocketClient.state(socket).assigns
510+
511+
assert assigns.error_code == 404
512+
assert assigns.error_reason == "incorrect uri used, please contact support"
513+
end
514+
end
515+
473516
describe "duplicate connections using the same device id" do
474517
@describetag :tmp_dir
475518

test/support/socket_client.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ defmodule SocketClient do
291291

292292
@impl Slipstream
293293
def handle_disconnect(
294-
{:error, {:upgrade_failure, %{reason: %{status_code: 401} = reason}}},
294+
{:error, {:upgrade_failure, %{reason: reason}}},
295295
socket
296296
) do
297297
socket =

0 commit comments

Comments
 (0)