# Remote Control Support Webapp — Plan Self-hosted TeamViewer-style remote support tool built on **VNC/RFB**, browser-based (noVNC), with invite links, access control, garagedoor SSO, and a persisted client list. Status legend: `[ ]` todo · `[~]` in progress · `[x]` done --- ## 1. Architecture ``` browser (noVNC) hub (this app) client machine ┌───────────────────────┐ ┌────────────────────────┐ ┌───────────────────┐ │ viewer.html │ wss │ express + ws │ │ VNC server :5900 │ │ RFB over WebSocket │◄──────►│ /vnc?ticket=… │ │ │ └───────────────────────┘ │ │ │ │ │ bridge: │ tcp │ │ │ direct mode ─────────┼───────►│ │ │ │ │ │ │ agent mode │ wss │ agent.js │ │ /agent (control) ◄───┼────────┤ outbound only │ │ /tunnel (data) ◄───┼────────┤ pipes to :5900 │ └────────────────────────┘ └───────────────────┘ │ SQLite (clients, grants, invites, sessions, audit) ``` **Two connection modes** — a client row is one or the other: | Mode | How | Use for | |---|---|---| | `direct` | Hub opens TCP to `host:port`. | LAN machines with a reachable VNC server. | | `agent` | Client runs `agent/agent.js`, dials **out** to the hub over WSS and holds it open. Hub asks it to open a data tunnel per session. | NAT'd / remote machines. The TeamViewer-shaped path. | **Why a proxy and not raw noVNC:** the hub terminates the RFB handshake itself. That lets it (a) authenticate to the real VNC server with a **server-side stored password** the browser never sees, and (b) enforce **view-only** by dropping input messages on the client→server stream. Both are required for invite links to be safe. ## 2. Tech stack - Node 22, CommonJS, Express 4 — matches `stash-ex-webapp`. - Deployed by Gitea Actions to Portainer's Swarm endpoint; single replica, pinned placement, named volume (see README). - `ws` for all WebSocket endpoints; Node's built-in `node:sqlite` for persistence (no native modules, so the container has no build stage). - `@novnc/novnc` vendored and served as browser ES modules. - Vanilla JS frontend in `public/` (no build step). - Auth delegated to **garagedoor-node-ws** (`http://192.168.4.208:8000`) — proxy pattern, never hold the JWT secret locally. ## 3. Data model (SQLite) - `clients` — id, name, mode, host/port, encrypted VNC password, agent key hash, `require_consent`, tags, os, agent_version, last_seen_at, status, created_by. - `grants` — per-user access to a client with a role and optional expiry. - `invites` — hashed token, kind (`enroll` | `session`), target client, role, max_uses, uses, expiry, revocation. - `sessions` — audit trail of every connection: who, what client, role, bytes, duration, source (`web` | `invite`), end reason. - `audit` — admin actions (client created/deleted, invite issued/revoked, grant changed). Secrets at rest (VNC passwords, agent keys) are AES-256-GCM encrypted with a key derived from `ENCRYPTION_KEY`. Invite and agent tokens are stored **hashed**, never plaintext. ## 4. Access control model Roles resolved per (user, client) at connect time: | Role | Can | |---|---| | `admin` | Everything: CRUD clients, issue/revoke invites, manage grants, view audit. | | `operator` | Connect with full keyboard/mouse control to granted clients. | | `viewer` | Connect **view-only** (input filtered at the proxy) to granted clients. | - Admin = garagedoor username in `ADMIN_USERS`, or garagedoor `level` ≥ `ADMIN_LEVEL`. - Non-admins see only clients they hold a grant for. - `require_consent` on a client makes the agent prompt the local user before each session. - Every WS connect uses a **one-time, 30-second ticket** minted by `POST /api/sessions`, so long-lived JWTs never appear in URLs or proxy logs. ## 5. Invite links — two kinds 1. **Enrollment invite** `/enroll/` — hand to a machine you want to manage. Page shows the install one-liner with a single-use token baked in; running it registers the machine as a client and it appears in the list. 2. **Session invite** `/s/` — hand to a person. Time-limited, use-limited access to **one** client at a fixed role (`viewer` or `operator`), no login required. This is the "send the customer a link" flow. Both are revocable, expiring, and logged. --- ## 6. Feature checklist ### Phase 1 — Foundation - [x] Project scaffold, package.json, env config, .gitignore - [x] SQLite schema + migrations on boot - [x] Secret encryption helper (AES-256-GCM) - [x] garagedoor auth proxy + `requireAuth` / `requireAdmin` middleware - [x] Login screen, token in localStorage, 401 → re-login ### Phase 2 — Clients & persistence - [x] `GET/POST/PATCH/DELETE /api/clients` - [x] Client list UI with status, tags, last-seen - [x] Add/edit client form (direct mode: host/port/password) - [x] Online/offline status tracking for agent clients ### Phase 3 — VNC bridge - [x] WS↔TCP bridge for direct mode - [x] Server-side RFB handshake + VNC Authentication (password never reaches browser) - [x] View-only enforcement by filtering client→server RFB messages - [x] noVNC viewer page: scaling, fullscreen, clipboard, Ctrl-Alt-Del - [x] One-time session tickets ### Phase 4 — Agent (NAT traversal) - [x] Agent control channel `/agent` with heartbeat + reconnect - [x] Data tunnel `/tunnel` paired to a waiting browser socket - [x] `agent/agent.js` — zero npm dependencies, uses Node 22's global WebSocket - [x] Enrollment via invite token → agent key issued once - [x] Local consent prompt when `require_consent` is set ### Phase 5 — Invites & access control - [x] Issue/list/revoke invites (both kinds) - [x] `/enroll/` enrollment page - [x] `/s/` session invite page (no login) - [x] Per-user grants CRUD - [x] Role resolution + enforcement at connect ### Phase 6 — Audit & operations - [x] Session history table + live "who is connected now" - [x] Audit log of admin actions - [x] Admin can force-disconnect an active session - [x] Dockerfile + docker-compose + Gitea Actions CI/CD to Portainer - [x] README with deploy + client setup instructions - [x] End-to-end test suite (`pnpm test`) — fake VNC server + fake auth service, 72 assertions covering auth, RBAC, VNC auth, view-only, invites, agent tunnel ### Phase 7 — Front end polish - [~] Modern visual design pass across all five pages - [~] Particle background (network-of-machines motif), reduced-motion aware, never rendered behind a live VNC canvas ### Phase 8 — Later / nice to have - [ ] File transfer between operator and client - [ ] Clipboard sync toggle per session - [ ] Multi-monitor selection - [ ] Session recording (RFB stream capture + replay) - [ ] Chat sidebar during a support session - [ ] Wake-on-LAN integration (sibling `wol-fleet-webapp`) - [ ] TOTP step-up before controlling a flagged client - [ ] Agent auto-update - [ ] RDP backend alongside VNC --- ## 7. Security notes - The hub is the only thing that knows VNC passwords; browsers get an RFB stream that has already cleared authentication. - View-only is enforced **server-side**, not by hiding UI. - Invite tokens: 32 bytes of `crypto.randomBytes`, stored as SHA-256, compared in constant time, single-use by default. - Deploy behind HTTPS (see the `lan-https-host` setup) — RFB over plain `ws://` is cleartext framebuffer data. - Known upstream debt in garagedoor (SQL injection, hardcoded secret) is *not* inherited: this app never touches that DB or that secret.