2 * @file MeshCentral File Browser WebSocket Proxy
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.
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
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
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
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
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}
51const WebSocket = require('ws');
52const url = require('url');
53const { getTenantMeshAPI } = require('./meshcentral');
54const db = require('../db');
57 * Initializes WebSocket server for MeshCentral file browser operations.
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.
64 * Connection URL format:
66 * ws://api.ibghub.com/api/meshcentral/files?sessionToken=abc123&agentId=456
69 * Message protocol (client -> MeshCentral):
78 * Message protocol (MeshCentral -> client):
85 * {"name": "file.txt", "size": 1024, "type": 3, "mtime": 1642012800},
86 * {"name": "folder", "size": 0, "type": 2, "mtime": 1642012800}
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
98 * @throws {Error} If WebSocket server initialization fails
100 * const http = require('http');
101 * const { setupFilesWebSocket } = require('./routes/meshcentral-files');
103 * const server = http.createServer(app);
104 * setupFilesWebSocket(server);
105 * server.listen(3000);
109 * Setup file browser WebSocket server
110 * @param {http.Server} server - HTTP server instance
112function setupFilesWebSocket(server) {
113 const wss = new WebSocket.Server({
115 path: '/meshcentral/files'
118 // Handle WebSocket upgrade
119 server.on('upgrade', (request, socket, head) => {
120 const pathname = url.parse(request.url).pathname;
122 if (pathname === '/api/meshcentral/files') {
123 wss.handleUpgrade(request, socket, head, (ws) => {
124 wss.emit('connection', ws, request);
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;
134 console.log(`[Files WebSocket] New connection: agentId=${agentId}, sessionToken=${sessionToken?.substring(0, 8)}...`);
136 if (!sessionToken || !agentId) {
137 console.error('[Files WebSocket] Missing sessionToken or agentId');
138 ws.close(1008, 'Missing required parameters');
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]
152 if (sessionResult.rows.length === 0) {
153 console.error('[Files WebSocket] Invalid or expired session');
154 ws.close(1008, 'Invalid or expired session');
158 const session = sessionResult.rows[0];
159 const nodeId = session.meshcentral_nodeid;
160 const tenantId = session.tenant_id;
162 console.log(`[Files WebSocket] Session validated: tenantId=${tenantId}, nodeId=${nodeId}`);
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');
172 console.log(`[Files WebSocket] Connected to MeshCentral for tenant ${tenantId}`);
174 // Forward messages from MeshCentral to client
175 api.ws.on('message', (data) => {
177 const message = JSON.parse(data.toString());
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') {
187 console.log(`[Files WebSocket] -> Client: ${message.action || message.type}`);
188 ws.send(JSON.stringify(message));
191 console.error('[Files WebSocket] Error parsing MeshCentral message:', err);
195 // Forward messages from client to MeshCentral
196 ws.on('message', (data) => {
198 const clientMessage = JSON.parse(data.toString());
199 console.log(`[Files WebSocket] <- Client: ${clientMessage.action}`, {
200 path: clientMessage.path,
201 reqid: clientMessage.reqid
204 // Build MeshCentral protocol message
205 const meshMessage = {
212 api.ws.send(JSON.stringify(meshMessage));
214 console.error('[Files WebSocket] Error forwarding client message:', err);
215 ws.send(JSON.stringify({
217 message: 'Failed to process request'
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
228 ws.on('error', (err) => {
229 console.error('[Files WebSocket] WebSocket error:', err);
232 // Send initial connection success
233 ws.send(JSON.stringify({
239 console.error('[Files WebSocket] Connection error:', err);
240 ws.close(1011, 'Internal server error');
244 console.log('[Files WebSocket] File browser WebSocket server setup complete');
247module.exports = { setupFilesWebSocket };