UNPKG

webssh2-server

Version:

A Websocket to SSH2 gateway using xterm.js, socket.io, ssh2

193 lines (147 loc) 8.09 kB
# WebSSH2 Admin API – Ideas & Feasibility ## Summary - Goal: Add an HTTP `/api` surface to administer live WebSSH2 sessions. - Core features: list active sessions; forcefully remove/close sessions; optionally send a message to the user before termination. - Feasibility: High. Requires an in‑memory session registry, minimal hooks in the Socket.IO layer, and an Express router for `/api`. No changes needed to the client for basic terminal messaging (we can write to the SSH stream). Optional UI notices would require client changes. ## Current State (Repo Review) - Entrypoint: `index.js``app/app.js` creates Express app, HTTP server, and Socket.IO. - Routes: `app/routes.js` mounts under `/ssh` (no REST API yet). - Sockets: `app/socket.js` creates a `WebSSH2Socket` per connection; maintains per‑socket `sessionState` and an `SSHConnection` instance (`app/ssh.js`). - Sessions: No global registry of active sessions; state is only on each Socket.IO `socket` instance. - Useful data available today: - `socket.id` (session id surrogate) - Client IP: `socket.handshake.address` or `socket.request.connection.remoteAddress` - Dest host/port: populated after `authenticate``initializeConnection` via `sessionState.{host,port}` - Username: `sessionState.username` (post‑auth) - Term size/type: `sessionState.{term,rows,cols}` - SSH stream handle for messaging: `ssh.stream` (when shell started) ## What We Need To Track Introduce an in‑memory registry keyed by `socket.id`: - id: Socket.IO id - user: username (null until auth) - source: { ip, port, userAgent? } - dest: { host, port } - term: { type, rows, cols } - timestamps: { connectedAt, authenticatedAt, lastActivityAt } - state flags: { authenticated, hasStream } - handles (internal, not exported): socket ref, ssh ref Update points in `app/socket.js`: - On `io.on('connection')`: register basic entry with source IP/UA, connectedAt. - On successful `initializeConnection`: update with username/dest; mark authenticated. - On `createShell` success: mark hasStream, store term/size. - On data/resize events: update `lastActivityAt`. - On `disconnect`/`handleConnectionClose`: remove from registry. ## Proposed API Surface - GET `/api/sessions` - Lists active sessions. - Response items: { id, user, source { ip, port }, dest { host, port }, term { type, rows, cols }, authenticated, connectedAt, authenticatedAt, lastActivityAt } - GET `/api/sessions/:id` - Details for one session. - DELETE `/api/sessions/:id` - Terminates a session. Body options: - { message?: string, notify?: { terminal?: boolean, delayMs?: number }, reason?: string } - Behavior: optionally send message to terminal, wait delay, close SSH stream and disconnect socket. - POST `/api/sessions/:id/message` - Sends a message to the user terminal (best‑effort; falls back to socket emit if no stream yet). - POST `/api/broadcast` - Sends message to multiple sessions with optional filters (by user, host). - GET `/api/stats` - Basic stats: counts by host/user, totals, uptime, memory. - GET `/api/health` - Health probe for orchestration. Notes - Fields exposing secrets must be masked; only metadata is returned. - Session id is the Socket.IO `socket.id`. ## Authentication & Security - Enable/disable API via config: `api.enabled` (default false). - Auth options (choose one, configurable): - Static API key via header `X-API-Key``api.key` (recommended minimal change). - HTTP Basic (reuse `createAuthMiddleware`) with a separate `api.user`/`api.password` pair. - mTLS (advanced deployments, optional later). - Network constraints: bind API under same server but guard with - CORS: reuse `config.getCorsConfig()` with a dedicated `http.apiOrigins` list. - Optional IP allowlist `api.allowlist`. - Rate limiting: express-rate-limit for write ops (terminate, message, broadcast). - Audit logging: log all admin actions with masked payloads. ## Remove/Message Semantics - Terminal message: If `ssh.stream` exists, `stream.write("\r\n*** Admin: <message> ***\r\n")` so it renders in terminal. If no stream yet, emit `socket.emit('data', ...)` to display text (client renders terminal from this event already). - Termination flow: message (optional) → optional `delayMs``ssh.end()` and `socket.disconnect(true)` → registry removal. - Return codes: 202 Accepted for async termination; 200 OK on immediate success; 404 if id not found. ## Implementation Plan (Phased) Phase 1 – Minimal, Safe, Useful - Add `app/session-registry.js` with: - register(socket), update(id, patch), remove(id), get(id), list(), terminate(id, opts), message(id, text), stats(). - Wire registry in `app/socket.js` lifecycle points. - Add `app/api-routes.js` (mount at `/api` in `createAppAsync` and `createApp` when `config.api.enabled`). - Endpoints: `GET /sessions`, `GET /sessions/:id`, `DELETE /sessions/:id`, `POST /sessions/:id/message`, `GET /stats`, `GET /health`. - Auth: simple API key check middleware. - Tests: Node test runner specs for registry and routes under `tests/api.test.js` (mock io/socket, inject fake sessions, verify JSON shape, and termination flow). Phase 2 – Hardening & Quality - Rate limits on write endpoints; structured audit logs; IP allowlist. - Better source IP extraction with trust proxy. - Config docs: update `CONFIG.md`, `ENV_VARIABLES.md`, `config.json.sample` with `api` section and env mappings. Phase 3 – Nice-to-haves - Broadcast messaging with filters. - Optional client UI toast via a new event (requires `webssh2_client` change). - Session tagging/metadata (e.g., who initiated, purpose). ## Config Additions (proposed) config.json ```json { "api": { "enabled": false, "key": null, "allowlist": [], "rateLimit": { "windowMs": 60000, "max": 30 } }, "http": { "apiOrigins": ["*:*"] } } ``` ENV (examples) - `WEBSSH2_API_ENABLED=true` - `WEBSSH2_API_KEY=supersecret` - `WEBSSH2_API_ALLOWLIST=10.0.0.0/8,127.0.0.1/32` - `WEBSSH2_HTTP_API_ORIGINS=https://admin.example.com:443` Notes: Update `app/envConfig.js` and `app/configSchema.js` to validate/merge these. ## Data Shapes (examples) GET /api/sessions ```json [ { "id": "kV2mI0yE7o0lq1ePAAAB", "user": "alice", "source": { "ip": "203.0.113.10", "port": 51234 }, "dest": { "host": "db01.internal", "port": 22 }, "term": { "type": "xterm-256color", "rows": 40, "cols": 120 }, "authenticated": true, "connectedAt": "2025-09-03T07:15:10.210Z", "authenticatedAt": "2025-09-03T07:15:12.422Z", "lastActivityAt": "2025-09-03T07:16:50.005Z" } ] ``` DELETE /api/sessions/:id body ```json { "message": "You will be disconnected by admin.", "notify": { "terminal": true, "delayMs": 1500 }, "reason": "maintenance" } ``` ## Risks & Edge Cases - Pre‑auth sockets: entries will lack user/dest; still listable; termination still works. - No `ssh.stream` yet: messaging falls back to socket emit; text appears in terminal after connect or may be dropped – acceptable for admin notices. - Proxies: ensure `app.set('trust proxy', true)` if using `X-Forwarded-For`; document. - Persistence: in‑memory registry resets on server restart; acceptable for admin ops. - Security: keep API disabled by default; require strong key; rate‑limit destructive ops. ## Test Strategy - Unit: registry behavior, masking, update/remove, termination flow with fakes. - Integration: spin up app in tests, call `/api/sessions` with seeded sockets (mock io), validate shapes and status codes. - E2E (optional): Playwright can exercise API with a running server if needed. ## Estimated Effort - Phase 1: ~3–6 hours including tests and docs. - Phase 2: ~2–4 hours (rate limit, allowlist, trust proxy handling). ## Open Questions - Should admin API live under the same origin or on a separate admin port? - Do we want role‑based control beyond a single API key? - Is there a requirement to persist session history/audit beyond logs? --- If you want, I can implement Phase 1 with config toggles, a registry, the routes, and tests.