EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
meshcentral-files.js
Go to the documentation of this file.
1/**
2 * @file MeshCentral File Browser WebSocket Proxy
3 * @description
4 * Provides secure WebSocket-based file management access to remote agents via MeshCentral
5 * integration. Acts as authentication proxy and message router between client browsers
6 * and MeshCentral's WebSocket API, eliminating iframe authentication complexity while
7 * maintaining multi-tenant security isolation.
8 *
9 * Key features:
10 * - **WebSocket proxy**: Bidirectional message forwarding between client and MeshCentral
11 * - **Session validation**: Verifies session tokens from meshcentral_sessions table
12 * - **Tenant isolation**: Each tenant uses separate MeshCentral API connection
13 * - **File operations**: ls, download, upload, rm, rename, mkdir
14 * - **Real-time updates**: Live directory listing updates and file operation status
15 *
16 * Protocol flow:
17 * 1. Client connects WebSocket with agentId and sessionToken query parameters
18 * 2. Backend validates session token existence and expiration in database
19 * 3. Backend retrieves agent's meshcentral_nodeid and tenant_id
20 * 4. Backend establishes connection to tenant's MeshCentral instance
21 * 5. Messages forwarded bidirectionally with automatic protocol translation
22 * 6. Session maintained until client disconnects or session expires
23 *
24 * Supported file actions (client -> MeshCentral):
25 * - **ls**: List files and directories in path
26 * - **download**: Download file by path
27 * - **upload**: Upload file to path
28 * - **rm**: Delete file or directory
29 * - **rename**: Rename/move file or directory
30 * - **mkdir**: Create new directory
31 *
32 * Message types (MeshCentral -> client):
33 * - **notify**: Operation status notifications
34 * - **download**: File download data stream
35 * - **uploaddone**: Upload completion confirmation
36 * - **ls**: Directory listing results
37 *
38 * Security:
39 * - Session token required for all connections
40 * - Session expiration enforced (expires_at check)
41 * - Tenant-based API isolation (no cross-tenant access)
42 * - Agent ID validated against session
43 * @module routes/meshcentral-files
44 * @requires ws - WebSocket server implementation
45 * @requires url - URL parsing for query parameters
46 * @requires ./meshcentral - Tenant MeshCentral API connection manager
47 * @requires ../db - PostgreSQL database connection for session validation
48 * @see {@link module:routes/meshcentral}
49 */
50
51const WebSocket = require('ws');
52const url = require('url');
53const { getTenantMeshAPI } = require('./meshcentral');
54const db = require('../db');
55
56/**
57 * Initializes WebSocket server for MeshCentral file browser operations.
58 *
59 * Sets up WebSocket upgrade handler on HTTP server to intercept connections at
60 * /api/meshcentral/files path. Creates isolated WebSocket connection per client,
61 * validates session credentials, establishes MeshCentral API connection, and
62 * maintains bidirectional message proxy until client disconnects.
63 *
64 * Connection URL format:
65 * ```
66 * ws://api.ibghub.com/api/meshcentral/files?sessionToken=abc123&agentId=456
67 * ```
68 *
69 * Message protocol (client -> MeshCentral):
70 * ```json
71 * {
72 * "action": "ls",
73 * "path": "/",
74 * "reqid": 1
75 * }
76 * ```
77 *
78 * Message protocol (MeshCentral -> client):
79 * ```json
80 * {
81 * "action": "ls",
82 * "reqid": 1,
83 * "path": "/",
84 * "dir": [
85 * {"name": "file.txt", "size": 1024, "type": 3, "mtime": 1642012800},
86 * {"name": "folder", "size": 0, "type": 2, "mtime": 1642012800}
87 * ]
88 * }
89 * ```
90 *
91 * Error handling:
92 * - Closes connection with code 1008 (policy violation) if missing parameters
93 * - Closes connection with code 1008 if session invalid/expired
94 * - Closes connection with code 1011 (internal error) if MeshCentral connection fails
95 * @function setupFilesWebSocket
96 * @param {http.Server} server - HTTP server instance to attach WebSocket upgrade handler
97 * @returns {void}
98 * @throws {Error} If WebSocket server initialization fails
99 * @example
100 * const http = require('http');
101 * const { setupFilesWebSocket } = require('./routes/meshcentral-files');
102 *
103 * const server = http.createServer(app);
104 * setupFilesWebSocket(server);
105 * server.listen(3000);
106 */
107
108/**
109 * Setup file browser WebSocket server
110 * @param {http.Server} server - HTTP server instance
111 */
112function setupFilesWebSocket(server) {
113 const wss = new WebSocket.Server({
114 noServer: true,
115 path: '/meshcentral/files'
116 });
117
118 // Handle WebSocket upgrade
119 server.on('upgrade', (request, socket, head) => {
120 const pathname = url.parse(request.url).pathname;
121
122 if (pathname === '/api/meshcentral/files') {
123 wss.handleUpgrade(request, socket, head, (ws) => {
124 wss.emit('connection', ws, request);
125 });
126 }
127 });
128
129 wss.on('connection', async (ws, request) => {
130 const params = url.parse(request.url, true).query;
131 const sessionToken = params.sessionToken;
132 const agentId = params.agentId;
133
134 console.log(`[Files WebSocket] New connection: agentId=${agentId}, sessionToken=${sessionToken?.substring(0, 8)}...`);
135
136 if (!sessionToken || !agentId) {
137 console.error('[Files WebSocket] Missing sessionToken or agentId');
138 ws.close(1008, 'Missing required parameters');
139 return;
140 }
141
142 try {
143 // Validate session and get device info
144 const sessionResult = await db.query(
145 `SELECT s.*, a.meshcentral_nodeid, a.tenant_id
146 FROM meshcentral_sessions s
147 JOIN agents a ON a.agent_id = s.agent_id
148 WHERE s.session_token = $1 AND s.agent_id = $2 AND s.expires_at > NOW()`,
149 [sessionToken, agentId]
150 );
151
152 if (sessionResult.rows.length === 0) {
153 console.error('[Files WebSocket] Invalid or expired session');
154 ws.close(1008, 'Invalid or expired session');
155 return;
156 }
157
158 const session = sessionResult.rows[0];
159 const nodeId = session.meshcentral_nodeid;
160 const tenantId = session.tenant_id;
161
162 console.log(`[Files WebSocket] Session validated: tenantId=${tenantId}, nodeId=${nodeId}`);
163
164 // Get tenant's MeshCentral API connection
165 const api = await getTenantMeshAPI(tenantId);
166 if (!api || !api.ws) {
167 console.error('[Files WebSocket] Failed to connect to MeshCentral');
168 ws.close(1011, 'MeshCentral connection failed');
169 return;
170 }
171
172 console.log(`[Files WebSocket] Connected to MeshCentral for tenant ${tenantId}`);
173
174 // Forward messages from MeshCentral to client
175 api.ws.on('message', (data) => {
176 try {
177 const message = JSON.parse(data.toString());
178
179 // Only forward file-related messages
180 if (message.type === 'notify' ||
181 message.type === 'download' ||
182 message.type === 'uploaddone' ||
183 message.action === 'ls' ||
184 message.action === 'download' ||
185 message.action === 'upload') {
186
187 console.log(`[Files WebSocket] -> Client: ${message.action || message.type}`);
188 ws.send(JSON.stringify(message));
189 }
190 } catch (err) {
191 console.error('[Files WebSocket] Error parsing MeshCentral message:', err);
192 }
193 });
194
195 // Forward messages from client to MeshCentral
196 ws.on('message', (data) => {
197 try {
198 const clientMessage = JSON.parse(data.toString());
199 console.log(`[Files WebSocket] <- Client: ${clientMessage.action}`, {
200 path: clientMessage.path,
201 reqid: clientMessage.reqid
202 });
203
204 // Build MeshCentral protocol message
205 const meshMessage = {
206 action: 'msg',
207 type: 'files',
208 nodeid: nodeId,
209 ...clientMessage
210 };
211
212 api.ws.send(JSON.stringify(meshMessage));
213 } catch (err) {
214 console.error('[Files WebSocket] Error forwarding client message:', err);
215 ws.send(JSON.stringify({
216 type: 'error',
217 message: 'Failed to process request'
218 }));
219 }
220 });
221
222 // Handle disconnection
223 ws.on('close', () => {
224 console.log(`[Files WebSocket] Client disconnected: agentId=${agentId}`);
225 // Note: We don't close api.ws as it may be shared by other connections
226 });
227
228 ws.on('error', (err) => {
229 console.error('[Files WebSocket] WebSocket error:', err);
230 });
231
232 // Send initial connection success
233 ws.send(JSON.stringify({
234 type: 'connected',
235 nodeId: nodeId
236 }));
237
238 } catch (err) {
239 console.error('[Files WebSocket] Connection error:', err);
240 ws.close(1011, 'Internal server error');
241 }
242 });
243
244 console.log('[Files WebSocket] File browser WebSocket server setup complete');
245}
246
247module.exports = { setupFilesWebSocket };