1// File operation request/response map
2const fileListRequests = new Map(); // requestId → { resolve, reject, timeout }
4// Utility to send file-list request to agent and await response
10function requestAgentFileList(agentId, path) {
11 return new Promise((resolve, reject) => {
12 const ws = agentConnections.get(agentId);
13 if (!ws || ws.readyState !== 1) return reject(new Error('Agent not connected'));
14 const requestId = 'filelist-' + Date.now() + '-' + Math.random().toString(36).slice(2);
16 fileListRequests.set(requestId, {
19 timeout: setTimeout(() => {
20 fileListRequests.delete(requestId);
21 reject(new Error('Timeout waiting for agent file list'));
25 ws.send(JSON.stringify({ type: 'file-list', requestId, path }));
29const { WebSocketServer } = require('ws');
30const url = require('url');
33 * agentConnections: agent_uuid → ws
34 * viewerConnections: sessionId → ws
35 * activeRdpSessions: sessionId → { agentId, viewer, agent, createdAt }
37const agentConnections = new Map();
38const viewerConnections = new Map();
39const activeRdpSessions = new Map();
45function initWebSocketServer(server) {
46 const wss = new WebSocketServer({ noServer: true });
48 // Handle upgrade requests
49 server.on('upgrade', (req, socket, head) => {
50 const pathname = req.url.split('?')[0];
54 pathname.startsWith('/api/rds/view/') ||
55 pathname.startsWith('/api/rds/control/')
57 wss.handleUpgrade(req, socket, head, (ws) =>
58 wss.emit('connection', ws, req)
63 // Handle all WebSocket connections through here
64 wss.on('connection', (ws, req) => {
65 const parsed = url.parse(req.url, true);
66 const path = parsed.pathname;
68 // ────────────────────────────────────────────
69 // 1. AGENT CONNECTION (/ws)
70 // ────────────────────────────────────────────
72 const agent_uuid = parsed.query.agent_uuid;
73 if (!agent_uuid) return ws.close();
75 agentConnections.set(agent_uuid, ws);
76 console.log(`[WS] Agent connected: ${agent_uuid}`);
78 ws.on('close', () => {
79 agentConnections.delete(agent_uuid);
80 console.log(`[WS] Agent disconnected: ${agent_uuid}`);
82 // Update last_seen in the database
84 const pool = require('./services/db');
86 'UPDATE agents SET last_seen = NOW() WHERE agent_uuid = $1',
89 console.log(`[WS] Updated last_seen for agent ${agent_uuid}`);
91 console.error(`[WS] Failed to update last_seen for agent ${agent_uuid}:`, err);
94 console.error(`[WS] Error updating last_seen for agent ${agent_uuid}:`, err);
97 // clean up any active RDS sessions belonging to this agent
98 for (const [sessionId, session] of activeRdpSessions.entries()) {
99 if (session.agentId === agent_uuid) {
100 if (session.viewer) session.viewer.close();
101 viewerConnections.delete(sessionId);
102 activeRdpSessions.delete(sessionId);
103 console.log(`[RDS] Cleaned orphaned session ${sessionId}`);
111 // ────────────────────────────────────────────
112 // 2. VIEWER CONNECTION (/api/rds/view/:sessionId)
113 // ────────────────────────────────────────────
114 if (path.startsWith('/api/rds/view/')) {
115 const sessionId = parsed.query.session;
116 if (!sessionId) return ws.close();
118 console.log(`[RDS] Viewer connected to session ${sessionId}`);
119 viewerConnections.set(sessionId, ws);
121 const session = activeRdpSessions.get(sessionId);
122 if (session) session.viewer = ws;
124 ws.on('close', () => {
125 viewerConnections.delete(sessionId);
126 console.log(`[RDS] Viewer disconnected: ${sessionId}`);
132 // ────────────────────────────────────────────
133 // 3. CONTROL CONNECTION (/api/rds/control/:sessionId)
134 // ────────────────────────────────────────────
135 if (path.startsWith('/api/rds/control/')) {
136 const sessionId = parsed.query.session;
137 if (!sessionId) return ws.close();
139 console.log(`[RDS] Control channel connected to session ${sessionId}`);
141 const session = activeRdpSessions.get(sessionId);
143 console.log(`[RDS] Session ${sessionId} not found, closing control channel`);
147 // Store control websocket reference
148 session.control = ws;
150 // Forward control messages (mouse/keyboard) to agent
151 ws.on('message', (raw) => {
153 const msg = JSON.parse(raw.toString());
154 console.log(`[RDS] Control message:`, msg.type);
156 // Get agent connection
157 const agentWs = agentConnections.get(session.agentId);
158 if (agentWs && agentWs.readyState === 1) {
159 // Forward to agent with sessionId
160 agentWs.send(JSON.stringify({
165 console.log(`[RDS] Agent not connected for session ${sessionId}`);
168 console.error('[RDS] Error forwarding control message:', err);
172 ws.on('close', () => {
173 if (session) session.control = null;
174 console.log(`[RDS] Control channel disconnected: ${sessionId}`);
180 // If path doesn't match anything
184 // ────────────────────────────────────────────
185 // FORWARDING PIPELINE (Viewer → Agent)
186 // ────────────────────────────────────────────
187 wss.on('connection', (ws, req) => {
188 ws.on('message', (raw) => {
190 // Viewer → Agent messages are JSON
191 const msg = JSON.parse(raw.toString());
192 if (!msg.sessionId) return;
194 const session = activeRdpSessions.get(msg.sessionId);
195 if (!session) return;
197 const agentWs = session.agent;
199 if (agentWs && agentWs.readyState === 1) {
200 agentWs.send(JSON.stringify(msg));
203 // Ignore invalid JSON (JPEG screen frames)
208 // ────────────────────────────────────────────
209 // FORWARDING PIPELINE (Agent → Viewer)
210 // ────────────────────────────────────────────
211 wss.on('connection', (ws, req) => {
212 ws.on('message', (raw) => {
213 // Try treat as agent-binary-screen-frame first
214 for (const [sessionId, session] of activeRdpSessions.entries()) {
215 if (session.agent === ws) {
216 const viewerWs = session.viewer;
217 if (viewerWs && viewerWs.readyState === 1) {
218 // Forward binary frames
224 // If this WAS JSON (clipboard events, file chunks, file-list, etc)
226 const msg = JSON.parse(raw.toString());
228 // Handle agent file-list response
229 if (msg.type === 'file-list-response' && msg.requestId) {
230 const reqObj = fileListRequests.get(msg.requestId);
232 clearTimeout(reqObj.timeout);
233 reqObj.resolve(msg.files || []);
234 fileListRequests.delete(msg.requestId);
239 // ...existing RDS clipboard/file chunk/session logic...
241 const session = activeRdpSessions.get(msg.sessionId);
242 if (!session) return;
243 const viewerWs = session.viewer;
244 if (viewerWs && viewerWs.readyState === 1) {
245 viewerWs.send(JSON.stringify(msg));
254 console.log('✅ WebSocket subsystem ready (agents + RDS)');
255 return { agentConnections, viewerConnections, activeRdpSessions, requestAgentFileList };
258module.exports = { initWebSocketServer, agentConnections, viewerConnections, activeRdpSessions, requestAgentFileList };