From 32d0e877d017c5a1494271fc0add75e4e31676a3 Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Mon, 27 Jul 2026 20:36:06 +0300 Subject: [PATCH] fix(remote-ws): route by the connection's device_id, not the payload's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commands from the miniplayer weren't reaching the player: play/pause STATE synced (track/status frames display regardless of id), but the transport commands went nowhere. Cause: device_message tagged broadcasts with the client-reported `device_id` from the payload. An old CLI (pre-0.7.1) clobbers its own id from a device_registered broadcast, so its tracks were tagged with the WEB's id; the miniplayer then targeted commands at that id and the server routed them back to the web (which ignores them), never to the CLI. Fix: use the connection's registered device_id (assigned at register, authoritative) for caching, primary, and the broadcast tag — ignore the payload id entirely. Robust regardless of client version. --- remote-ws/lib/remote_ws/ws/handler.ex | 15 ++++++++++----- remote-ws/test/remote_ws/ws/handler_test.exs | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/remote-ws/lib/remote_ws/ws/handler.ex b/remote-ws/lib/remote_ws/ws/handler.ex index 20bf5af8..2a8fdc02 100644 --- a/remote-ws/lib/remote_ws/ws/handler.ex +++ b/remote-ws/lib/remote_ws/ws/handler.ex @@ -133,12 +133,17 @@ defmodule RemoteWs.Ws.Handler do # ---- device message: track / status (handler.ts lines 64-283) ---- - defp device_message(%{"data" => data, "device_id" => device_id, "token" => token}, state) - when is_map(data) do + defp device_message(%{"data" => data, "token" => token}, state) when is_map(data) do case Auth.verify_token(token) do - {:ok, %{did: did}} when is_binary(did) -> - name = - Devices.name_of(did, device_id) || Devices.name_of(did, state.device_id) || "websocket" + # Use the CONNECTION's registered device_id — the authoritative routing id + # assigned at register — NOT the `device_id` the client puts in the payload. + # A client's self-reported id can be wrong (e.g. an old CLI that clobbered + # its own id from a device_registered broadcast); that would tag its tracks + # with another device's id, so the miniplayer would send commands to the + # wrong place (they'd never reach the player) even though state still syncs. + {:ok, %{did: did}} when is_binary(did) and is_binary(state.device_id) -> + device_id = state.device_id + name = Devices.name_of(did, device_id) || "websocket" data = if data["type"] == "track" do diff --git a/remote-ws/test/remote_ws/ws/handler_test.exs b/remote-ws/test/remote_ws/ws/handler_test.exs index 8fb3738a..bd77a551 100644 --- a/remote-ws/test/remote_ws/ws/handler_test.exs +++ b/remote-ws/test/remote_ws/ws/handler_test.exs @@ -125,6 +125,26 @@ defmodule RemoteWs.Ws.HandlerTest do assert env["data"]["liked"] == false end + test "a track message is tagged with the CONNECTION's device_id, not the payload's" do + did = new_did() + token = Token.sign(%{"did" => did}) + cli = FakeDevice.start(did, "real-cli", "Rocksky CLI", self()) + st = %{device_id: "real-cli", did: did} + RedisMemory.put("primary_device:#{did}", "real-cli") + + # The payload lies about its device_id — the server must ignore it and use the + # connection's registered id, so commands from the miniplayer route correctly. + data = %{"type" => "track", "title" => "t", "artist" => "a", "album" => "al"} + + Handler.handle( + %{"type" => "message", "data" => data, "device_id" => "spoofed", "token" => token}, + st + ) + + assert_receive {:pushed, ^cli, frame} + assert Jason.decode!(frame)["device_id"] == "real-cli" + end + test "an invalid token is ignored (no reply, no broadcast)" do did = new_did() _dev = FakeDevice.start(did, "a", "A", self()) -- 2.51.2