Skip to content

fix(websocket): the commands of a user go to the devices of the user only - #1072

Merged
azarouski merged 1 commit into
developfrom
fix/device-channels
Oct 28, 2026
Merged

azarouski merged 1 commit into
developfrom
fix/device-channels

Conversation

@azarouski

Copy link
Copy Markdown
Contributor

Stacked on #1071 (fix/malformed-messages: the guard of the listeners and its rate-limited log, the same lines of lib/units/websocket/index.ts): merge it first, then retarget this one to develop.

Problem

The client of the websocket names the channel of 53 events and the websocket pushes their message to it: input.*, shell.command, device.install/uninstall, connect.start (remote ADB), fs.retrieve/fs.list, screen.capture, clipboard.copy, forward.*, group.kick, ... A device runs every message of its channel without an owner check, and the channel of a device is base64(sha1(serial)) (the solo plugins of Android and iOS), computable from the serial every user sees.

So any signed-in user controlled any device: a shell, installs, files, the screen, remote ADB - also of a device used by another user or reserved by the grid for an automation session (its tests sabotaged, its app data read).

The client also subscribed the websocket to any channel (tx.punch, the response channel of every transaction), e.g. the private group channel of another user, where the devices of the user publish their logs and notifications.

Changes

lib/units/websocket/zebrunner-channels.ts - the middleware of the socket of every client (socket.use, one line in index.ts):

Event Allowed when
the 52 device events but group.invite the channel is the one of a present device the user uses now, or the group channel of the user (all the devices of the user); an admin: any device
group.invite the device of the channel is in a group of the user (groups.subscribed, as the device list); an admin: any
tx.punch, tx.cleanup, the response channel of the transaction events a transaction channel of the UI, tx.<uuid>
  • The devices of the user come from the database (getAll(email, {index: 'owner'}), present devices only): loaded again after 2 s (a released device gets no command later than 2 s after it), or after 250 ms for a channel not among them (a device taken just now gets its first command; a client cannot load the database in a loop).
  • The messages of a client are checked one after another in their order (the order of the touch events matters).
  • A refused message is logged once a minute per event and reason with the user (fix(units): a malformed message no longer ends the unit #1071) and dropped.
  • lib/db/zebrunner.ts: ownedDeviceChannels, visibleDeviceChannel.
  • ZEBRUNNER.md: the feature.

The UI sends every command to device.channel of a device it uses and its transactions are tx.<uuid> (res/app/src/core/transaction.ts): nothing changes for it. The REST API checks the owner by the serial already.

Tests

  • test/units/zebrunner/websocket-channels.mts (13): every event of WebsocketClientEvents of index.ts is classified (a new upstream event fails the test until it is); commands to the device of the user and to its group channel pass, to another device and without a channel are refused, an admin passes; invite by the groups; transaction channels only; the order kept while the devices are loaded (one load); a released device refused after 2 s, a device taken just now passes; a database error drops; a real socket.io server and client; the queries of the database
  • the queries against a real RethinkDB (stf migrate): only the present devices of the user, the visibility by the groups
  • npm run lint, npm run test:unit (512 passing)

…only

The client names the channel of the 53 device events (input.*,
shell.command, device.install, connect.start, fs.retrieve,
screen.capture, ...) and a device runs every command of its channel.
The channel of a device is base64(sha1(serial)), so any signed-in user
controlled any device: also the devices of the other users and of the
automation of the grid, with a shell, installs, files and remote ADB.
The client also subscribed to any channel, e.g. the private group
channel of another user, where the devices of the user publish their
logs.

lib/units/websocket/zebrunner-channels.ts, the middleware of the socket
of every client (socket.use):
- a command goes to the channel of a present device the user uses or to
  the group channel of the user; an admin sends to any device
- group.invite goes to a device of the groups of the user
- tx.punch, tx.cleanup and the response channel of a transaction are
  channels of transactions (tx.<uuid>) only
- the devices of the user are loaded from the database (the owner index)
  again after 2 s, or after 250 ms for a device not among them (a device
  taken just now); the messages of a client are checked in their order
- a refused message is logged once a minute (zebrunner-guard) and dropped

A test checks that every event of WebsocketClientEvents is classified,
so a new upstream event cannot skip the check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

❌ STF CI report

5 failing  ·  6 test tiers (6 pass / 0 fail / 0 skip)  ·  10 Android versions (10 with a usable device in STF)  ·  6 iOS versions (1 passing)

Test tiers

Tier Result
✅ Build (npm + webpack) deps cache miss
✅ Lint (eslint + jsonlint + translations + workflow shell) lib/, res/ and root JS + JSON, translation catalogs, and every workflow run: block
✅ Unit tests (mocha) lib/util, lib/wire and lib/db specs
✅ Component tests (vitest + React) 104 specs, 0 failed, 0 errored
✅ Integration + device-less E2E (stf local + RethinkDB + Playwright + SeaweedFS) stf local success, fake devices success, web UI suite success, storage success
✅ Docker Compose + AVD + Playwright (Android 16, API 36) 21 Playwright tests, image success, Compose/AVD success, teardown success

Android device matrix

Android API Image Boot In STF Usable SIM IDs Screen Touch Shell UI E2E Teardown
✅ 12 31 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 12L 32 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 13 33 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 14 34 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 15 35 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 16 36 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 16.1 36.1 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 17 37.0 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 17 37.1 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
✅ 17 37.2 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅

Image = a system image is published for that API and arch. Boot = emulator reached sys.boot_completed and answers adb. In STF = the device registered with the STF provider. Usable = STF offered it as Use and handed over control. SIM IDs = imei, imsi, iccid and phoneNumber landed on the device document, which from API 23 up needs an STFService build whose agent answers --telephony (a leg whose emulator reports no usable SIM records a pass, because there is nothing to read). Screen = minicap frames reached the browser canvas and kept changing. Touch = a browser tap and swipe showed up in getevent on the device, at the coordinates they were aimed at: the tap within 5% of the centre of the touch axes, the swipe descending monotonically from 0.75 to 0.25 (the emulator runs with adb root, which minitouch needs to open /dev/input). Shell = the dashboard shell widget ran a command on it. UI E2E = the rest of the Playwright web UI suite. Teardown = the emulator action shut the AVD down and exited on its own.

iOS simulator matrix

iOS Runtime Xcode Simulator WDA Session Appium kept Size Orientation Home Touch Screenshot Settings Clipboard URL Stream Stable
❌ 15 15.5 16.4 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ➖ ✅ ✅
❌ 16 16.4 16.4 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ➖ ✅ ✅
❌ 17 17.5 16.4 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅
❌ 18 18.6 16.4 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ❌ ✅ ✅ ✅
✅ 26 26.5 26.6 ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅
❌ 27 27.1 27.1 ✅ ❌ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖

WebDriverAgent on an iPhone simulator against the WDA client of the ios-device unit (test/zebrunner-ios). A red column: Simulator, the simulator did not boot · WDA, WebDriverAgent did not start · Session, no WDA session · Appium kept, the session of an Appium test was not kept · Size, wrong screen size · Orientation, no orientation or battery · Home, the home button did not work · Touch, tap or swipe failed · Screenshot, no screenshot · Settings, Settings did not open or the app was lost · Clipboard, the pasteboard was not written or read · URL, Safari did not open the address · Stream, no MJPEG frame · Stable, the unit would have stopped

Failures

  • iOS 15: the pasteboard was not written or read: expected 'com.facebook.WebDriverAgentRunner.xct…' to equal 'com.apple.springboard'
  • iOS 16: the pasteboard was not written or read: expected 'com.facebook.WebDriverAgentRunner.xct…' to equal 'com.apple.springboard'
  • iOS 17: the pasteboard was not written or read: expected 'com.facebook.WebDriverAgentRunner.xct…' to equal 'com.apple.springboard'
  • iOS 18: the pasteboard was not written or read: expected 'com.facebook.WebDriverAgentRunner.xct…' to equal 'com.apple.springboard'
  • iOS 27: WebDriverAgent did not start

Logs, screenshots, logcat and Playwright traces are attached to the run as artifacts · full run · commit 7cb44e9

Base automatically changed from fix/malformed-messages to develop October 28, 2026 12:56
@azarouski
azarouski merged commit 2e3c8a1 into develop Oct 28, 2026
43 of 54 checks passed
@azarouski
azarouski deleted the fix/device-channels branch October 28, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant