EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
wsServer.js
Go to the documentation of this file.
1// File operation request/response map
2const fileListRequests = new Map(); // requestId → { resolve, reject, timeout }
3
4// Utility to send file-list request to agent and await response
5/**
6 *
7 * @param agentId
8 * @param path
9 */
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);
15 // Store resolver
16 fileListRequests.set(requestId, {
17 resolve,
18 reject,
19 timeout: setTimeout(() => {
20 fileListRequests.delete(requestId);
21 reject(new Error('Timeout waiting for agent file list'));
22 }, 10000)
23 });
24 // Send request
25 ws.send(JSON.stringify({ type: 'file-list', requestId, path }));
26 });
27}
28// backend/wsServer.js
29const { WebSocketServer } = require('ws');
30const url = require('url');
31
32/**
33 * agentConnections: agent_uuid → ws
34 * viewerConnections: sessionId → ws
35 * activeRdpSessions: sessionId → { agentId, viewer, agent, createdAt }
36 */
37const agentConnections = new Map();
38const viewerConnections = new Map();
39const activeRdpSessions = new Map();
40
41/**
42 *
43 * @param server
44 */
45function initWebSocketServer(server) {
46 const wss = new WebSocketServer({ noServer: true });
47
48 // Handle upgrade requests
49 server.on('upgrade', (req, socket, head) => {
50 const pathname = req.url.split('?')[0];
51
52 if (
53 pathname === '/ws' ||
54 pathname.startsWith('/api/rds/view/') ||
55 pathname.startsWith('/api/rds/control/')
56 ) {
57 wss.handleUpgrade(req, socket, head, (ws) =>
58 wss.emit('connection', ws, req)
59 );
60 }
61 });
62
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;
67
68 // ────────────────────────────────────────────
69 // 1. AGENT CONNECTION (/ws)
70 // ────────────────────────────────────────────
71 if (path === '/ws') {
72 const agent_uuid = parsed.query.agent_uuid;
73 if (!agent_uuid) return ws.close();
74
75 agentConnections.set(agent_uuid, ws);
76 console.log(`[WS] Agent connected: ${agent_uuid}`);
77
78 ws.on('close', () => {
79 agentConnections.delete(agent_uuid);
80 console.log(`[WS] Agent disconnected: ${agent_uuid}`);
81
82 // Update last_seen in the database
83 try {
84 const pool = require('./services/db');
85 pool.query(
86 'UPDATE agents SET last_seen = NOW() WHERE agent_uuid = $1',
87 [agent_uuid]
88 ).then(() => {
89 console.log(`[WS] Updated last_seen for agent ${agent_uuid}`);
90 }).catch(err => {
91 console.error(`[WS] Failed to update last_seen for agent ${agent_uuid}:`, err);
92 });
93 } catch (err) {
94 console.error(`[WS] Error updating last_seen for agent ${agent_uuid}:`, err);
95 }
96
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}`);
104 }
105 }
106 });
107
108 return;
109 }
110
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();
117
118 console.log(`[RDS] Viewer connected to session ${sessionId}`);
119 viewerConnections.set(sessionId, ws);
120
121 const session = activeRdpSessions.get(sessionId);
122 if (session) session.viewer = ws;
123
124 ws.on('close', () => {
125 viewerConnections.delete(sessionId);
126 console.log(`[RDS] Viewer disconnected: ${sessionId}`);
127 });
128
129 return;
130 }
131
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();
138
139 console.log(`[RDS] Control channel connected to session ${sessionId}`);
140
141 const session = activeRdpSessions.get(sessionId);
142 if (!session) {
143 console.log(`[RDS] Session ${sessionId} not found, closing control channel`);
144 return ws.close();
145 }
146
147 // Store control websocket reference
148 session.control = ws;
149
150 // Forward control messages (mouse/keyboard) to agent
151 ws.on('message', (raw) => {
152 try {
153 const msg = JSON.parse(raw.toString());
154 console.log(`[RDS] Control message:`, msg.type);
155
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({
161 ...msg,
162 sessionId
163 }));
164 } else {
165 console.log(`[RDS] Agent not connected for session ${sessionId}`);
166 }
167 } catch (err) {
168 console.error('[RDS] Error forwarding control message:', err);
169 }
170 });
171
172 ws.on('close', () => {
173 if (session) session.control = null;
174 console.log(`[RDS] Control channel disconnected: ${sessionId}`);
175 });
176
177 return;
178 }
179
180 // If path doesn't match anything
181 ws.close();
182 });
183
184 // ────────────────────────────────────────────
185 // FORWARDING PIPELINE (Viewer → Agent)
186 // ────────────────────────────────────────────
187 wss.on('connection', (ws, req) => {
188 ws.on('message', (raw) => {
189 try {
190 // Viewer → Agent messages are JSON
191 const msg = JSON.parse(raw.toString());
192 if (!msg.sessionId) return;
193
194 const session = activeRdpSessions.get(msg.sessionId);
195 if (!session) return;
196
197 const agentWs = session.agent;
198
199 if (agentWs && agentWs.readyState === 1) {
200 agentWs.send(JSON.stringify(msg));
201 }
202 } catch {
203 // Ignore invalid JSON (JPEG screen frames)
204 }
205 });
206 });
207
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
219 viewerWs.send(raw);
220 }
221 }
222 }
223
224 // If this WAS JSON (clipboard events, file chunks, file-list, etc)
225 try {
226 const msg = JSON.parse(raw.toString());
227
228 // Handle agent file-list response
229 if (msg.type === 'file-list-response' && msg.requestId) {
230 const reqObj = fileListRequests.get(msg.requestId);
231 if (reqObj) {
232 clearTimeout(reqObj.timeout);
233 reqObj.resolve(msg.files || []);
234 fileListRequests.delete(msg.requestId);
235 }
236 return;
237 }
238
239 // ...existing RDS clipboard/file chunk/session logic...
240 if (msg.sessionId) {
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));
246 }
247 }
248 } catch {
249 // Not JSON → ignore
250 }
251 });
252 });
253
254 console.log('✅ WebSocket subsystem ready (agents + RDS)');
255 return { agentConnections, viewerConnections, activeRdpSessions, requestAgentFileList };
256}
257
258module.exports = { initWebSocketServer, agentConnections, viewerConnections, activeRdpSessions, requestAgentFileList };