remote desktop only shuts off whoever is currently connected instead of all. File browser stuff is now per user.

This commit is contained in:
KingKDot
2026-03-30 20:27:12 -05:00
parent acbacb50d5
commit 0b64d92fe5
2 changed files with 43 additions and 16 deletions
@@ -315,17 +315,24 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`); logger.debug(`[rd] ignoring duplicate desktop_start for client ${clientId}`);
} }
break; break;
case "desktop_stop": case "desktop_stop": {
sendDesktopCommand(target, "desktop_stop", {}); const otherViewers = sessionManager.getRdSessionsForClient(clientId)
if (state.isStreaming) { .filter(s => s.id !== ws.data.sessionId);
state.isStreaming = false; if (otherViewers.length === 0) {
rdStreamingState.set(clientId, state); sendDesktopCommand(target, "desktop_stop", {});
logger.debug(`[rd] stopped streaming for client ${clientId}`); 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 { } else {
rdStreamingState.set(clientId, { ...state, isStreaming: false }); logger.debug(`[rd] ignoring desktop_stop for client ${clientId} - ${otherViewers.length} other viewer(s) still active`);
logger.debug(`[rd] stop requested while not streaming for client ${clientId}`);
} }
break; break;
}
case "desktop_select_display": { case "desktop_select_display": {
const newDisplay = Number(payload.display) || 0; const newDisplay = Number(payload.display) || 0;
if (state.display !== newDisplay) { if (state.display !== newDisplay) {
@@ -29,6 +29,13 @@ type WsViewerClusterDeps = {
consumeHttpDownloadPayload: (payload: any) => Promise<void> | void; consumeHttpDownloadPayload: (payload: any) => Promise<void> | void;
}; };
const fileBrowserCommandSessions = new Map<string, string>();
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 { function decodeViewerPayload(raw: string | ArrayBuffer | Uint8Array): any | null {
if (typeof raw === "string") { if (typeof raw === "string") {
try { try {
@@ -85,39 +92,41 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
if (typeof payload.commandType !== "string") return; if (typeof payload.commandType !== "string") return;
logger.debug(`[DEBUG] Handling command type: ${payload.commandType}`); logger.debug(`[DEBUG] Handling command type: ${payload.commandType}`);
const actualPayload = payload.payload || {}; const actualPayload = payload.payload || {};
const routedId = payload.id || commandId;
if (ws.data.sessionId) trackFileBrowserCommand(routedId, ws.data.sessionId);
switch (payload.commandType) { switch (payload.commandType) {
case "file_read": case "file_read":
logger.debug(`[DEBUG] Forwarding file_read to client ${clientId}:`, actualPayload.path); 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"); metrics.recordCommand("file_read");
break; break;
case "file_write": 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"); metrics.recordCommand("file_write");
break; break;
case "file_search": 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"); metrics.recordCommand("file_search");
break; break;
case "file_copy": 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"); metrics.recordCommand("file_copy");
break; break;
case "file_move": 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"); metrics.recordCommand("file_move");
break; break;
case "file_chmod": 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"); metrics.recordCommand("file_chmod");
break; break;
case "file_execute": case "file_execute":
logger.debug(`[DEBUG] Forwarding file_execute to client ${clientId}:`, actualPayload.path); 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"); metrics.recordCommand("file_execute");
break; break;
case "file_upload_http": 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"); metrics.recordCommand("file_upload");
logAudit({ logAudit({
timestamp: Date.now(), timestamp: Date.now(),
@@ -137,6 +146,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
switch (payload.type) { switch (payload.type) {
case "file_list": 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)); target.ws.send(encodeMessage({ type: "command", commandType: "file_list", id: commandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_list"); metrics.recordCommand("file_list");
logAudit({ logAudit({
@@ -150,6 +160,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
}); });
break; break;
case "file_download": 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)); target.ws.send(encodeMessage({ type: "command", commandType: "file_download", id: commandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_download"); metrics.recordCommand("file_download");
logAudit({ logAudit({
@@ -177,6 +188,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
} }
case "file_delete": { case "file_delete": {
const deleteCommandId = payload.commandId || commandId; 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)); target.ws.send(encodeMessage({ type: "command", commandType: "file_delete", id: deleteCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_delete"); metrics.recordCommand("file_delete");
logAudit({ logAudit({
@@ -192,6 +204,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
} }
case "file_mkdir": { case "file_mkdir": {
const mkdirCommandId = payload.commandId || commandId; 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)); target.ws.send(encodeMessage({ type: "command", commandType: "file_mkdir", id: mkdirCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_mkdir"); metrics.recordCommand("file_mkdir");
logAudit({ logAudit({
@@ -207,6 +220,7 @@ export function handleFileBrowserViewerMessage(ws: ServerWebSocket<SocketData>,
} }
case "file_zip": { case "file_zip": {
const zipCommandId = payload.commandId || commandId; 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)); target.ws.send(encodeMessage({ type: "command", commandType: "file_zip", id: zipCommandId, payload: { path: payload.path || "" } } as any));
metrics.recordCommand("file_zip"); metrics.recordCommand("file_zip");
logAudit({ logAudit({
@@ -239,6 +253,9 @@ export function handleFileBrowserMessage(clientId: string, payload: any, deps: W
void deps.consumeHttpDownloadPayload(payload); void deps.consumeHttpDownloadPayload(payload);
} }
const payloadCommandId = typeof payload?.commandId === "string" ? payload.commandId : undefined;
const ownerSessionId = payloadCommandId ? fileBrowserCommandSessions.get(payloadCommandId) : undefined;
let hasSession = false; let hasSession = false;
for (const session of sessionManager.getFileBrowserSessionsByClient(clientId)) { for (const session of sessionManager.getFileBrowserSessionsByClient(clientId)) {
if (!hasSession) { if (!hasSession) {
@@ -250,6 +267,9 @@ export function handleFileBrowserMessage(clientId: string, payload: any, deps: W
if (isHttpDownload) { if (isHttpDownload) {
continue; continue;
} }
if (ownerSessionId && session.id !== ownerSessionId) {
continue;
}
if (payload.type === "file_download" && payload.data) { if (payload.type === "file_download" && payload.data) {
const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data); const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data);
safeSendViewer(session.viewer, { ...payload, data }); safeSendViewer(session.viewer, { ...payload, data });