EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshcentral-terminal.js
Go to the documentation of this file.
1/**
2 * @file MeshCentral Terminal WebSocket Proxy
3 * @description
4 * Provides secure WebSocket-based remote terminal/console access to RMM agents via MeshCentral
5 * integration. Acts as authentication proxy and message router between client web terminals
6 * and MeshCentral's WebSocket API, enabling browser-based shell access (CMD, PowerShell, Bash)
7 * without iframe complexity while maintaining multi-tenant security isolation.
8 *
9 * Key features:
10 * - **WebSocket proxy**: Bidirectional message forwarding for terminal I/O
11 * - **Session validation**: Verifies session tokens from meshcentral_sessions table
12 * - **Tenant isolation**: Each tenant uses separate MeshCentral API connection
13 * - **Multiple shell types**: Windows CMD, PowerShell, Linux Bash, macOS Terminal
14 * - **Real-time interaction**: Full terminal emulation with ANSI color support
15 * - **No iframe needed**: Direct API integration eliminates cross-origin issues
16 *
17 * Protocol flow:
18 * 1. Client connects WebSocket with `agentId` and `sessionToken` query parameters
19 * 2. Backend validates session token existence and expiration in database
20 * 3. Backend retrieves agent's `meshcentral_nodeid` and `tenant_id`
21 * 4. Backend establishes WebSocket connection to tenant's MeshCentral instance
22 * 5. Backend sends terminal request message to MeshCentral (type: 'console')
23 * 6. Terminal I/O messages forwarded bidirectionally until disconnection
24 * 7. Session maintained with keepalive pings until client disconnects
25 *
26 * Terminal types supported (platform-dependent):
27 * - **Windows**: CMD (default), PowerShell
28 * - **Linux**: Bash, sh, zsh (depends on agent configuration)
29 * - **macOS**: Terminal (zsh/bash)
30 *
31 * Message protocol (client -> MeshCentral):
32 * - **Keystroke**: `{ type: 'console', data: 'command\\n' }`
33 * - **Resize**: `{ type: 'console', cols: 80, rows: 24 }`
34 * - **Close**: WebSocket close frame
35 *
36 * Message protocol (MeshCentral -> client):
37 * - **Output**: `{ type: 'console', data: 'output text' }`
38 * - **Connected**: `{ type: 'console', msg: 'Terminal Ready' }`
39 * - **Disconnected**: `{ type: 'console', msg: 'Disconnected' }`
40 *
41 * Security:
42 * - Session token required for all connections
43 * - Session expiration enforced (`expires_at` check)
44 * - Tenant-based API isolation (no cross-tenant terminal access)
45 * - Agent ID validated against session
46 * @module routes/meshcentral-terminal
47 * @requires ws - WebSocket server implementation
48 * @requires ../middleware/auth - JWT authentication middleware
49 * @requires ../db - PostgreSQL database connection for session validation
50 * @requires ./meshcentral - Tenant MeshCentral API connection manager
51 * @see {@link module:routes/meshcentral}
52 * @see {@link module:routes/meshcentral-files}
53 */
54
55/**
56 * MeshCentral Terminal WebSocket Proxy
57 * Provides secure WebSocket connection to MeshCentral terminal
58 * No iframe needed - direct API integration
59 */
60
61const WebSocket = require('ws');
62const authenticateToken = require('../middleware/auth');
63const db = require('../db');
64const { getTenantMeshAPI } = require('./meshcentral');
65
66/**
67 * Initializes WebSocket server for MeshCentral remote terminal/console operations.
68 *
69 * Sets up WebSocket upgrade handler on HTTP server to intercept connections at
70 * /api/meshcentral/terminal path. Creates isolated WebSocket connection per client,
71 * validates session credentials, establishes MeshCentral API connection, sends terminal
72 * request to agent, and maintains bidirectional terminal I/O proxy until disconnection.
73 *
74 * Connection URL format:
75 * ```
76 * ws://api.ibghub.com/api/meshcentral/terminal?sessionToken=abc123&agentId=456
77 * ```
78 *
79 * Terminal I/O flow:
80 * ```
81 * Client Keystroke -> Backend WS -> MeshCentral API -> Agent Terminal
82 * Client Terminal <- Backend WS <- MeshCentral API <- Agent Output
83 * ```
84 *
85 * Example terminal session:
86 * ```javascript
87 * // Client connects
88 * const ws = new WebSocket('ws://api.ibghub.com/api/meshcentral/terminal?sessionToken=xxx&agentId=123');
89 *
90 * // Send command
91 * ws.send(JSON.stringify({ type: 'console', data: 'dir\\n' }));
92 *
93 * // Receive output
94 * ws.onmessage = (event) => {
95 * const msg = JSON.parse(event.data);
96 * if (msg.type === 'console') {
97 * console.log(msg.data); // Terminal output with ANSI codes
98 * }
99 * };
100 * ```
101 *
102 * Error handling:
103 * - Sends error message and closes with code 1000 if missing parameters
104 * - Sends error message and closes if session invalid/expired
105 * - Sends error message and closes if MeshCentral connection fails
106 * - Keepalive pings prevent idle connection timeouts
107 * @function setupTerminalWebSocket
108 * @param {http.Server} server - HTTP server instance to attach WebSocket upgrade handler
109 * @returns {void}
110 * @throws {Error} If WebSocket server initialization fails
111 * @example
112 * const http = require('http');
113 * const { setupTerminalWebSocket } = require('./routes/meshcentral-terminal');
114 *
115 * const server = http.createServer(app);
116 * setupTerminalWebSocket(server);
117 * server.listen(3000);
118 */
119
120/**
121 * Setup WebSocket server for terminal connections
122 * @param {Express.Application} app - Express app instance
123 * @param {http.Server} server - HTTP server instance
124 */
125function setupTerminalWebSocket(server) {
126 const wss = new WebSocket.Server({
127 noServer: true,
128 path: '/meshcentral/terminal'
129 });
130
131 // Handle WebSocket upgrade
132 server.on('upgrade', (request, socket, head) => {
133 if (request.url.startsWith('/api/meshcentral/terminal')) {
134 wss.handleUpgrade(request, socket, head, (ws) => {
135 wss.emit('connection', ws, request);
136 });
137 }
138 });
139
140 wss.on('connection', async (ws, request) => {
141 console.log('🔌 Terminal WebSocket connection established');
142
143 try {
144 // Extract sessionToken from URL query
145 const url = new URL(request.url, 'http://localhost');
146 const sessionToken = url.searchParams.get('sessionToken');
147 const agentId = url.searchParams.get('agentId');
148
149 if (!sessionToken || !agentId) {
150 ws.send(JSON.stringify({ error: 'Missing sessionToken or agentId' }));
151 ws.close();
152 return;
153 }
154
155 // Validate session
156 const sessionResult = await db.query(
157 'SELECT s.*, a.meshcentral_nodeid, a.tenant_id FROM meshcentral_sessions s JOIN agents a ON s.agent_id = a.agent_id WHERE s.session_token = $1 AND s.expires_at > NOW()',
158 [sessionToken]
159 );
160
161 if (sessionResult.rows.length === 0) {
162 ws.send(JSON.stringify({ error: 'Invalid or expired session' }));
163 ws.close();
164 return;
165 }
166
167 const session = sessionResult.rows[0];
168 const nodeId = session.meshcentral_nodeid;
169 const tenantId = session.tenant_id;
170
171 console.log(`✅ Terminal session validated: nodeId=${nodeId}`);
172
173 // Get tenant's MeshCentral API client
174 const api = await getTenantMeshAPI(tenantId);
175
176 // Connect to MeshCentral WebSocket
177 await api.connectWebSocket();
178
179 console.log('🔌 Connected to MeshCentral WebSocket');
180
181 // Request terminal access to the device
182 // MeshCentral protocol: JSON messages with action and nodeid
183 const terminalRequest = {
184 action: 'msg',
185 type: 'console',
186 nodeid: nodeId,
187 msg: JSON.stringify({
188 type: 'console',
189 action: 'start'
190 })
191 };
192
193 api.ws.send(JSON.stringify(terminalRequest));
194
195 // Forward messages from MeshCentral to client
196 api.ws.on('message', (data) => {
197 try {
198 const message = JSON.parse(data.toString());
199
200 // Filter terminal output messages
201 if (message.type === 'console' || message.action === 'msg') {
202 ws.send(JSON.stringify(message));
203 }
204 } catch (err) {
205 console.error('Error parsing MeshCentral message:', err);
206 }
207 });
208
209 // Forward messages from client to MeshCentral (terminal input)
210 ws.on('message', (data) => {
211 try {
212 const message = JSON.parse(data.toString());
213
214 // Wrap client messages for MeshCentral
215 const meshMessage = {
216 action: 'msg',
217 type: 'console',
218 nodeid: nodeId,
219 msg: JSON.stringify(message)
220 };
221
222 api.ws.send(JSON.stringify(meshMessage));
223 } catch (err) {
224 console.error('Error forwarding to MeshCentral:', err);
225 }
226 });
227
228 // Handle disconnection
229 ws.on('close', () => {
230 console.log('🔌 Terminal WebSocket disconnected');
231 if (api.ws && api.ws.readyState === WebSocket.OPEN) {
232 // Send close terminal command
233 const closeMsg = {
234 action: 'msg',
235 type: 'console',
236 nodeid: nodeId,
237 msg: JSON.stringify({ action: 'close' })
238 };
239 api.ws.send(JSON.stringify(closeMsg));
240 }
241 });
242
243 ws.on('error', (error) => {
244 console.error('Terminal WebSocket error:', error);
245 });
246
247 } catch (error) {
248 console.error('❌ Terminal setup error:', error);
249 ws.send(JSON.stringify({ error: 'Failed to setup terminal', message: error.message }));
250 ws.close();
251 }
252 });
253
254 console.log('✅ Terminal WebSocket server setup complete');
255}
256
257module.exports = { setupTerminalWebSocket };