Skip to content

Feature/rig bridge security hardening & other enhancements#803

Merged
accius merged 10 commits intoaccius:Stagingfrom
ceotjoe:feature/rig-bridge-security-hardening
Mar 22, 2026
Merged

Feature/rig bridge security hardening & other enhancements#803
accius merged 10 commits intoaccius:Stagingfrom
ceotjoe:feature/rig-bridge-security-hardening

Conversation

@ceotjoe
Copy link
Contributor

@ceotjoe ceotjoe commented Mar 22, 2026

What does this PR do?

Summary

Ports all security fixes from feature/rig-bridge-security-assessment-fixes into a clean rewrite on top of Staging, aligns the two Staging-only plugins (SmartSDR, RTL-SDR) with the same security standards, and adds a zero-friction WSJT-X relay credential setup flow.


Security hardening (rig-bridge)

Authentication

  • Auto-generates a random 32-hex apiToken on first run via crypto.randomBytes
  • requireAuth Express middleware validates X-RigBridge-Token on all write endpoints
  • First-run: token is server-injected into the setup page HTML for seamless auto-login; a welcome banner lets the user copy it before it disappears
  • Setup UI gains a full login overlay, logout link, and shake animation on bad token

Rate limiting & input validation

  • Sliding-window rate limiters per route group (freq, mode, ptt, cfg, token)
  • Frequency validated to 1 kHz–75 GHz; mode to alphanumeric 1–20 chars
  • POST /api/config validates host strings and port ranges for all 5 plugin types

SSRF prevention

  • isValidRemoteHost() / isValidPort() helpers added to server.js
  • All six plugins (flrig, rigctld, tci, wsjtx-relay, smartsdr, rtl-tcp) validate the configured host before opening any connection

New endpoints

  • POST /api/auth/verify, GET /api/token, POST /api/token/regenerate, POST /api/setup/token-seen
  • GET /api/log/stream now authenticates via ?token= query param (EventSource can't set headers)
  • GET /api/config strips apiToken from the response

Plugin updates

  • rigctld.js — dual-mode protocol support, fixSplit option
  • tci.js — full rewrite with native WebSocket + ws npm fallback
  • wsjtx-relay.js — UDP binds to 127.0.0.1 by default; udpBindAddress config field

WSJT-X relay credential flow

Previously, users had to manually find and copy two opaque strings (relay key + session ID) across two different UIs. Now there are two one-click paths:

Option A — Push from OpenHamClock (recommended)
Settings → Station → Rig Control → WSJT-X Relay → Configure Relay on Rig Bridge
OpenHamClock fetches the relay key from its own server and POSTs both credentials (key + session ID) directly to the connected rig-bridge in one step.

Option B — Fetch from rig-bridge setup UI
Integrations tab → enter OHC URL → 🔗 Fetch credentials
rig-bridge pulls the relay key automatically; help text directs the user to copy their session ID from OHC Settings.

Changes:

  • GET /api/wsjtx/relay-credentials endpoint on OHC server (CORS open for localhost only)
  • New WSJT-X Relay sub-section in SettingsPanel.jsx with session ID display + push button
  • fetchWsjtxCredentials() JS function in rig-bridge setup UI
  • 10 new i18n keys across all 15 language files

Docs

rig-bridge/README.md updated with SmartSDR and RTL-SDR plugin sections, three-option WSJT-X relay setup guide, updated troubleshooting table, and corrected project structure tree.


Frontend (OHC)

  • RigContext.jsxX-RigBridge-Token header on all rig API calls; 401 → error: 'unauthorized'
  • RigControlPanel.jsx — two-tier error banner (unauthorized vs. daemon unreachable)
  • SettingsPanel.jsx — API token field with password show/hide toggle

Type of change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactor / code cleanup
  • Documentation
  • Translation
  • Map layer plugin

Checklist

  • App loads without console errors
  • Tested in Dark, Light, and Retro themes
  • Responsive at different screen sizes (desktop + mobile)
  • If touching server.js: caches have TTLs and size caps (we serve 2,000+ concurrent users)
  • If adding an API route: includes caching and error handling
  • If adding a panel: wired into Modern, Classic, and Dockable layouts
  • No hardcoded colors — uses CSS variables (var(--accent-cyan), etc.)
  • No .bak, .old, console.log debug lines, or test scripts included

I'm still working on this, just to give a bit overview where the changes are.

ceotjoe and others added 4 commits March 22, 2026 10:20
…anch

Reimplements all security fixes from feature/rig-bridge-security-assessment-fixes
onto Staging as a clean rewrite, preserving Staging's SmartSDR/RTL-TCP additions.

rig-bridge/core/config.js:
- Auto-generate apiToken via crypto.randomBytes on first run
- Add tokenDisplayed, fixSplit, udpBindAddress to DEFAULT_CONFIG
- Merge smartsdr/rtltcp config sections in loadConfig()

rig-bridge/core/server.js:
- Add isValidRemoteHost(), isValidPort(), makeRateLimiter() helpers
- Add requireAuth middleware (X-RigBridge-Token header)
- Add per-route rate limiters (freq/mode/ptt/cfg/token)
- GET /api/log/stream: authenticate via ?token= query param
- GET /api/ports: require auth
- GET /api/config: strip apiToken from response
- POST /api/config: require auth + SSRF host/port validation for all 5 plugin hosts
- POST /api/test, /api/logging: require auth
- POST /freq, /mode, /ptt: require auth + rate limit + input validation
- New endpoints: POST /api/auth/verify, GET /api/token,
  POST /api/token/regenerate, POST /api/setup/token-seen
- buildSetupHtml: login overlay, welcome banner, first-run token injection,
  full auth flow (doAutoLogin, doLogin, doLogout, authHeaders)

rig-bridge/plugins/*.js:
- flrig.js: add defensive host validation before XML-RPC connect
- rigctld.js: dual-mode protocol, fixSplit support, host validation
- tci.js: full WebSocket implementation (ws npm + native), host validation
- wsjtx-relay.js: UDP bind to 127.0.0.1 by default, udpBindAddress config
- rtl-tcp.js, smartsdr.js: add host validation (Staging-only plugins)

src/:
- RigContext.jsx: restore X-RigBridge-Token auth headers + 401 handling
- RigControlPanel.jsx: restore two-tier error banner (unauthorized vs daemon)
- SettingsPanel.jsx: restore API token field with show/hide toggle
- lang/en.json + 14 other languages: add 4 API token i18n keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add supported radio entries, setup sections, config field tables,
expected log output, troubleshooting rows, and project structure
entries for the two new Staging plugins (smartsdr.js, rtl-tcp.js).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates manual relay key and session ID copy-paste with two
complementary auto-configure flows.

OHC server (server/routes/wsjtx.js):
- GET /api/wsjtx/relay-credentials — returns WSJTX_RELAY_KEY to
  callers on localhost origins (CORS opened for local rig-bridge UI)
- OPTIONS preflight handler for the same route

OHC frontend:
- SettingsPanel.jsx: new WSJT-X Relay sub-section inside the Rig
  Control card (visible when rig control is enabled)
  - Shows session ID (read-only) with copy button
  - "Configure Relay on Rig Bridge" button: fetches relay key from
    /api/wsjtx/relay-credentials, then POSTs url+key+session+enabled
    to the configured rig-bridge /api/config in one click
  - Inline success/error feedback
- App.jsx: passes wsjtx.sessionId down to SettingsPanel
- 15 lang files: 10 new i18n keys for the WSJT-X relay UI

rig-bridge setup UI (rig-bridge/core/server.js):
- "Fetch credentials" button next to the OHC Server URL field
  auto-fills the relay key via GET {url}/api/wsjtx/relay-credentials
- Session ID help text directs users to OHC Settings to copy it
- fetchWsjtxCredentials() JS function with full error handling

rig-bridge/README.md:
- Replaces bare config table with three-option setup guide
  (Option A: push from OHC, Option B: fetch in rig-bridge UI,
   Option C: manual config)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The regex /\/$/ inside the buildSetupHtml template literal had its
backslash silently dropped (\/ is an unrecognized escape in template
literals, producing just /). This turned /\/$/ into //$/ which starts
a line comment in the generated HTML, making fetch() unclosed and
crashing the entire <script> block with 'Unexpected keyword if'.

Fix: use a character class /[/]$/ instead of an escaped slash, which
avoids the backslash-in-template-literal pitfall entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ceotjoe
Copy link
Contributor Author

ceotjoe commented Mar 22, 2026

@accius probably there are some conflicts between @alanhargreaves (#804) and mine. In case just merge Alans first and I will adjust mine then accordingly.

ceotjoe and others added 5 commits March 22, 2026 14:04
PTT polling:
- poll() now sends cmd 0x1c sub 0x00 (read TX state) on every tick,
  staggered 100 ms after the mode query
- handleData() gains case 0x1c to parse the response and call
  updateState('ptt', ...), so externally keyed TX (VOX, foot switch)
  is reflected in the UI without waiting for a write command

DV mode:
- 0x17 was mapped to 'DATA-FM'; corrected to 'DV' to match the
  rig-listener implementation (D-STAR voice mode on IC-705, IC-9700)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…esponses

Two fragile checks could falsely report PTT active:

1. IF; parser: `txState !== '0'` treated any unexpected byte at position 22
   (garbage on reconnect, model-specific extension, truncation artifact, or
   a bare empty string) as PTT ON. Changed to only assert ON for an explicit
   '1' (PTT TX) or '2' (CAT/linear TX).

2. TX; case: a bare `TX;` with no digit produced txDigit = '', which failed
   the `=== '0'` guard and fell through to updateState('ptt', true).
   Now only '1' or '2' assert ON; missing digit is silently ignored.

Both changes follow the same principle: require a positive match to assert
TX state rather than inferring it from the absence of '0'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces English fallback text added in the credential-flow commit with
proper translations for all 10 WSJT-X relay keys across:
ca, de, es, fr, it, ja, ka, ko, ms, nl, pt, ru, sl, zh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When rig-bridge returns 403 on a PTT command (pttEnabled: false in its
config), the UI now clearly tells the user why PTT isn't working instead
of silently reverting the button.

RigContext.jsx:
- Detect res.status === 403 in setPTT, set error('ptt-disabled'), and
  revert the optimistic PTT state — same pattern as the 401 handler
- Clear ptt-disabled error on next successful PTT response

RigControlPanel.jsx:
- Amber warning banner (distinct from the red daemon/auth banners)
  shown when error === 'ptt-disabled'
- PTT button gains a 🔒 prefix, dashed border, and tooltip with the
  full message so the hint is visible even on small panels
- ptt-disabled excluded from the generic daemon-error fallback banner

i18n:
- New key app.rigControl.error.pttDisabled in en.json and all 14
  language files with proper translations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both rig-bridge's WSJT-X relay and a locally-running OpenHamClock
server bind to UDP 2237. Added a callout in the WSJT-X Relay section
explaining that rig-bridge must be started first, what the 'port already
in use' log message means, and how to recover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two root causes were producing spurious PTT=true in the Yaesu USB plugin:

1. IF; response PTT field is position-unreliable across models.
   The TX/RX flag at position 22 was confirmed only on the FT-991A.
   On FT-891, FT-710, FT-DX10 etc. the "unknown" byte at position 4
   may be absent, shifting all subsequent fields left by one — so the
   memory channel digit ('1' for channels 100-199) lands at position 22
   and triggers PTT=TX. Remove PTT parsing from case 'IF' entirely.

2. Missed auto-info TX0; (unkey) had no short-term recovery path.
   The 30-second keepalive was the only mechanism to recover a stuck
   PTT=true after the radio released TX.

Fix: add TX; (bare = read query, not set command) to the startup
sequence (IF;TX;) and to the 30-second keepalive (AI1;IF;TX;).
PTT state is now sourced exclusively from TX;/RX; responses, which
use an unambiguous 3-character format (TX0;/TX1;/TX2;) that is
consistent across all FT-series models.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@accius
Copy link
Owner

accius commented Mar 22, 2026

Great work @ceotjoe — this is a thorough and well-structured PR. Reviewed all 33 files.

Security hardening — solid implementation

The token auth flow is well done. crypto.randomBytes(16) for generation, first-run banner with one-time display,
X-RigBridge-Token header validation on write endpoints, and backward compatibility for existing installs with no
token. The sliding-window rate limiters with per-route granularity (freq 20/s, mode 10/s, PTT 5/s, config 10/min) are
sensible limits. isValidRemoteHost() and isValidPort() plus the duplicated checks in plugin connect functions give
good defense-in-depth.

The UDP bind change from 0.0.0.0 to 127.0.0.1 by default is an important security improvement.

WSJT-X relay credential flow — clean

The dual-direction approach (push from OHC settings, or fetch from rig-bridge setup UI) covers both workflows nicely.
The CORS restriction to localhost origins on /api/wsjtx/relay-credentials is correct. This eliminates a real friction
point in the setup process.

Protocol fixes — these are important

The Yaesu PTT fix is well-documented and explains exactly why the IF; response is unreliable across models. Moving to
dedicated TX; queries and requiring explicit TX1/TX2 digits prevents the false-positive TX detection. The Icom CI-V
PTT polling addition (cmd 0x1c sub 0x00) and DV mode correction are clean. The rigctld multi-line response parsing fix
and optional fixSplit workaround show good understanding of the protocol edge cases.

Minor notes for future consideration:

  1. The first-run token injection via JSON.stringify is safe since the token is hex-only from crypto.randomBytes —
    worth a comment noting why for future maintainers.
  2. Might be worth a README note that the token protects against unauthorized commands but is transmitted in plaintext
    over HTTP — fine for LAN use, just setting expectations.

Merging now. @alanhargreaves heads up — #804 will need a rebase onto Staging after this lands since both PRs touch
wsjtx.js and SettingsPanel.jsx. 73!

@accius accius merged commit 6a44918 into accius:Staging Mar 22, 2026
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.

2 participants