wire protocol is generated now instead of being hard coded with versioning for commands.

This commit is contained in:
KingKDot
2026-07-30 12:38:28 -05:00
parent 3f49eb07f7
commit 477b9a5353
27 changed files with 2628 additions and 204 deletions
@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import {
getCommandCompatibility,
getNegotiatedCommandVersions,
requireCommandVersion,
sanitizeCommandVersionRanges,
versionCommandForClient,
} from "./command-compatibility";
describe("command version negotiation", () => {
test("sanitizes advertised ranges and ignores unknown commands", () => {
expect(sanitizeCommandVersionRanges({
process_list: { min: 1, max: 3 },
desktop_start: { min: 3, max: 2 },
not_a_command: { min: 1, max: 1 },
})).toEqual({
process_list: { min: 1, max: 3 },
});
});
test("treats pre-3.0 agents as v1 for backward compatibility", () => {
expect(getCommandCompatibility({}, "process_list")).toEqual({
supported: true,
version: 1,
server: { min: 1, max: 1 },
agent: { min: 1, max: 1 },
legacyAssumption: true,
});
});
test("selects the highest shared version", () => {
const client = {
commandVersions: {
process_list: { min: 1, max: 3 },
},
};
expect(requireCommandVersion(client, "process_list")).toBe(1);
expect(getNegotiatedCommandVersions(client)).toEqual({ process_list: 1 });
expect(versionCommandForClient(client, {
type: "command",
commandType: "process_list",
id: "command-123",
})).toMatchObject({
commandType: "process_list",
commandVersion: 1,
});
});
test("strictly reports missing commands and non-overlapping versions", () => {
expect(getCommandCompatibility(
{ commandVersions: {} },
"process_list",
)).toMatchObject({
supported: false,
reason: "agent_missing_command",
});
const incompatible = {
commandVersions: {
process_list: { min: 2, max: 3 },
},
};
expect(getCommandCompatibility(incompatible, "process_list")).toMatchObject({
supported: false,
reason: "no_common_version",
});
expect(() => requireCommandVersion(incompatible, "process_list")).toThrow(
"no shared version",
);
});
});
@@ -0,0 +1,120 @@
import {
COMMAND_TYPES,
COMMAND_VERSION_SUPPORT,
isCommandType,
type CommandType,
type CommandVersionRange,
} from "./generated/wire-contract";
import type { ClientInfo } from "./types";
import type { Command } from "./protocol";
export type CommandCompatibility = {
supported: boolean;
version?: number;
server: CommandVersionRange;
agent?: CommandVersionRange;
reason?: "agent_missing_command" | "no_common_version";
legacyAssumption?: boolean;
};
function cleanPositiveInt(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
const result = Math.floor(value);
return result > 0 && result <= 65_535 ? result : undefined;
}
export function sanitizeCommandVersionRanges(
value: unknown,
): Partial<Record<CommandType, CommandVersionRange>> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const result: Partial<Record<CommandType, CommandVersionRange>> = {};
let count = 0;
for (const [name, rawRange] of Object.entries(value as Record<string, unknown>)) {
if (count >= COMMAND_TYPES.length || !isCommandType(name)) continue;
if (!rawRange || typeof rawRange !== "object" || Array.isArray(rawRange)) continue;
const min = cleanPositiveInt((rawRange as any).min);
const max = cleanPositiveInt((rawRange as any).max);
if (min === undefined || max === undefined || min > max) continue;
result[name] = { min, max };
count += 1;
}
return result;
}
export function getCommandCompatibility(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: CommandType,
): CommandCompatibility {
const server = COMMAND_VERSION_SUPPORT[command];
const advertised = client.commandVersions;
// Pre-3.0 agents did not advertise a catalog. They remain v1 best-effort.
if (!advertised) {
const supported = server.min <= 1 && server.max >= 1;
return supported
? { supported: true, version: 1, server, agent: { min: 1, max: 1 }, legacyAssumption: true }
: { supported: false, server, agent: { min: 1, max: 1 }, reason: "no_common_version", legacyAssumption: true };
}
const agent = advertised[command];
if (!agent) {
return { supported: false, server, reason: "agent_missing_command" };
}
const min = Math.max(server.min, agent.min);
const max = Math.min(server.max, agent.max);
if (min > max) {
return { supported: false, server, agent, reason: "no_common_version" };
}
return { supported: true, version: max, server, agent };
}
export function getCommandCompatibilityCatalog(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
): Record<CommandType, CommandCompatibility> {
return Object.fromEntries(
COMMAND_TYPES.map((command) => [command, getCommandCompatibility(client, command)]),
) as Record<CommandType, CommandCompatibility>;
}
export function getNegotiatedCommandVersions(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
): Partial<Record<CommandType, number>> {
const result: Partial<Record<CommandType, number>> = {};
for (const command of COMMAND_TYPES) {
const compatibility = getCommandCompatibility(client, command);
if (compatibility.supported && compatibility.version !== undefined) {
result[command] = compatibility.version;
}
}
return result;
}
export function requireCommandVersion(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: CommandType,
): number {
const compatibility = getCommandCompatibility(client, command);
if (!compatibility.supported || compatibility.version === undefined) {
const reason = compatibility.reason === "agent_missing_command"
? "agent does not advertise this command"
: `no shared version (server ${compatibility.server.min}-${compatibility.server.max}, agent ${compatibility.agent?.min ?? "none"}-${compatibility.agent?.max ?? "none"})`;
throw new Error(`Command ${command} is unsupported: ${reason}`);
}
return compatibility.version;
}
/**
* Use this at target-aware send boundaries. It fails before transmission when
* the connected agent does not support the command and pins the negotiated
* version into the envelope.
*/
export function versionCommandForClient(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: Command,
): Command {
return {
...command,
commandVersion: requireCommandVersion(client, command.commandType),
};
}
@@ -0,0 +1,879 @@
// Code generated by scripts/generate-wire-protocol.ts; DO NOT EDIT.
export const WIRE_SCHEMA_VERSION = 1 as const;
export const WIRE_PROTOCOL_VERSION = 1 as const;
export const WIRE_MESSAGE_TYPES = [
"backstage_browser_check_result",
"backstage_browser_launch_status",
"backstage_clone_progress",
"backstage_dxgi_status",
"backstage_installed_apps_result",
"backstage_lookup_result",
"backstage_window_list_result",
"client_logs_result",
"clipboard_content",
"command",
"command_abort",
"command_progress",
"command_result",
"console_output",
"desktop_audio_uplink",
"desktop_cursor",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"disconnect_info",
"enrollment_challenge",
"enrollment_status",
"file_dirsize_result",
"file_download",
"file_hash_result",
"file_icon_result",
"file_list_result",
"file_peek_result",
"file_read_result",
"file_search_result",
"file_thumb_result",
"file_upload_result",
"frame",
"frame_ack",
"hello",
"hello_ack",
"keylog_clear_result",
"keylog_delete_result",
"keylog_file_content",
"keylog_file_list",
"keylog_permission_result",
"notification",
"notification_config",
"ping",
"plugin_event",
"pong",
"process_icon_result",
"process_list_result",
"proxy_close",
"proxy_data",
"screenshot_result",
"script_result",
"status",
"voice_uplink",
"webcam_devices",
"webrtc_p2p_answer",
"webrtc_p2p_ice"
] as const;
export type MessageKind = (typeof WIRE_MESSAGE_TYPES)[number];
export const SERVER_TO_AGENT_MESSAGE_TYPES = [
"command",
"command_abort",
"enrollment_challenge",
"enrollment_status",
"frame_ack",
"hello_ack",
"notification_config",
"ping",
"plugin_event",
"pong"
] as const;
export type ServerToAgentMessageType = (typeof SERVER_TO_AGENT_MESSAGE_TYPES)[number];
export const AGENT_TO_SERVER_MESSAGE_TYPES = [
"backstage_browser_check_result",
"backstage_browser_launch_status",
"backstage_clone_progress",
"backstage_dxgi_status",
"backstage_installed_apps_result",
"backstage_lookup_result",
"backstage_window_list_result",
"client_logs_result",
"clipboard_content",
"command_progress",
"command_result",
"console_output",
"desktop_audio_uplink",
"desktop_cursor",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"disconnect_info",
"file_dirsize_result",
"file_download",
"file_hash_result",
"file_icon_result",
"file_list_result",
"file_peek_result",
"file_read_result",
"file_search_result",
"file_thumb_result",
"file_upload_result",
"frame",
"hello",
"keylog_clear_result",
"keylog_delete_result",
"keylog_file_content",
"keylog_file_list",
"keylog_permission_result",
"notification",
"ping",
"plugin_event",
"pong",
"process_icon_result",
"process_list_result",
"proxy_close",
"proxy_data",
"screenshot_result",
"script_result",
"status",
"voice_uplink",
"webcam_devices",
"webrtc_p2p_answer",
"webrtc_p2p_ice"
] as const;
export type AgentToServerMessageType = (typeof AGENT_TO_SERVER_MESSAGE_TYPES)[number];
export const COMMAND_TYPES = [
"agent_update",
"backstage_browser_check",
"backstage_enable_cursor",
"backstage_enable_dxgi",
"backstage_enable_keyboard",
"backstage_enable_mouse",
"backstage_enable_uia",
"backstage_installed_apps",
"backstage_key_down",
"backstage_key_up",
"backstage_kill_all",
"backstage_lookup",
"backstage_mouse_down",
"backstage_mouse_move",
"backstage_mouse_up",
"backstage_mouse_wheel",
"backstage_request_keyframe",
"backstage_select_display",
"backstage_set_fps",
"backstage_set_quality",
"backstage_set_resolution",
"backstage_start",
"backstage_start_browser_injected",
"backstage_start_chrome_injected",
"backstage_start_process",
"backstage_start_process_injected",
"backstage_stop",
"backstage_window_list",
"client_logs_request",
"clipboard_set",
"clipboard_sync_start",
"clipboard_sync_stop",
"console_input",
"console_resize",
"console_start",
"console_stop",
"darwin_request_permissions",
"desktop_audio_start",
"desktop_audio_stop",
"desktop_enable_cursor",
"desktop_enable_keyboard",
"desktop_enable_mouse",
"desktop_encoder_capabilities",
"desktop_key_down",
"desktop_key_up",
"desktop_mouse_down",
"desktop_mouse_move",
"desktop_mouse_up",
"desktop_mouse_wheel",
"desktop_request_keyframe",
"desktop_select_display",
"desktop_set_bitrate",
"desktop_set_duplication",
"desktop_set_fps",
"desktop_set_profile",
"desktop_set_quality",
"desktop_set_resolution",
"desktop_start",
"desktop_stop",
"desktop_text",
"disconnect",
"elevate",
"file_chmod",
"file_copy",
"file_delete",
"file_dirsize",
"file_download",
"file_execute",
"file_hash",
"file_icon",
"file_list",
"file_mkdir",
"file_move",
"file_peek",
"file_read",
"file_request_access",
"file_search",
"file_thumb",
"file_upload",
"file_upload_http",
"file_write",
"file_zip",
"keylog_clear_all",
"keylog_delete",
"keylog_list",
"keylog_request_permission",
"keylog_retrieve",
"plugin_load",
"plugin_load_http",
"plugin_unload",
"privacy_start",
"privacy_status",
"privacy_stop",
"process_icon",
"process_kill",
"process_list",
"process_resume",
"process_suspend",
"proxy_close",
"proxy_connect",
"proxy_data",
"reconnect",
"screenshot",
"script_exec",
"silent_exec",
"uninstall",
"virtual_enable_cursor",
"virtual_enable_keyboard",
"virtual_enable_mouse",
"virtual_key_down",
"virtual_key_up",
"virtual_kill_all",
"virtual_mouse_down",
"virtual_mouse_move",
"virtual_mouse_up",
"virtual_mouse_wheel",
"virtual_request_keyframe",
"virtual_set_fps",
"virtual_set_quality",
"virtual_start",
"virtual_start_process",
"virtual_stop",
"virtual_window_list",
"voice_capabilities",
"voice_downlink",
"voice_session_start",
"voice_session_stop",
"webcam_list",
"webcam_select",
"webcam_set_fps",
"webcam_set_quality",
"webcam_start",
"webcam_stop",
"webrtc_p2p_ice",
"webrtc_p2p_offer",
"webrtc_p2p_stop",
"webrtc_publish",
"webrtc_stop",
"winre_install",
"winre_probe",
"winre_uninstall"
] as const;
export type CommandType = (typeof COMMAND_TYPES)[number];
export type CommandVersionRange = Readonly<{ min: number; max: number }>;
export const COMMAND_VERSION_SUPPORT = {
"agent_update": {
"min": 1,
"max": 1
},
"backstage_browser_check": {
"min": 1,
"max": 1
},
"backstage_enable_cursor": {
"min": 1,
"max": 1
},
"backstage_enable_dxgi": {
"min": 1,
"max": 1
},
"backstage_enable_keyboard": {
"min": 1,
"max": 1
},
"backstage_enable_mouse": {
"min": 1,
"max": 1
},
"backstage_enable_uia": {
"min": 1,
"max": 1
},
"backstage_installed_apps": {
"min": 1,
"max": 1
},
"backstage_key_down": {
"min": 1,
"max": 1
},
"backstage_key_up": {
"min": 1,
"max": 1
},
"backstage_kill_all": {
"min": 1,
"max": 1
},
"backstage_lookup": {
"min": 1,
"max": 1
},
"backstage_mouse_down": {
"min": 1,
"max": 1
},
"backstage_mouse_move": {
"min": 1,
"max": 1
},
"backstage_mouse_up": {
"min": 1,
"max": 1
},
"backstage_mouse_wheel": {
"min": 1,
"max": 1
},
"backstage_request_keyframe": {
"min": 1,
"max": 1
},
"backstage_select_display": {
"min": 1,
"max": 1
},
"backstage_set_fps": {
"min": 1,
"max": 1
},
"backstage_set_quality": {
"min": 1,
"max": 1
},
"backstage_set_resolution": {
"min": 1,
"max": 1
},
"backstage_start": {
"min": 1,
"max": 1
},
"backstage_start_browser_injected": {
"min": 1,
"max": 1
},
"backstage_start_chrome_injected": {
"min": 1,
"max": 1
},
"backstage_start_process": {
"min": 1,
"max": 1
},
"backstage_start_process_injected": {
"min": 1,
"max": 1
},
"backstage_stop": {
"min": 1,
"max": 1
},
"backstage_window_list": {
"min": 1,
"max": 1
},
"client_logs_request": {
"min": 1,
"max": 1
},
"clipboard_set": {
"min": 1,
"max": 1
},
"clipboard_sync_start": {
"min": 1,
"max": 1
},
"clipboard_sync_stop": {
"min": 1,
"max": 1
},
"console_input": {
"min": 1,
"max": 1
},
"console_resize": {
"min": 1,
"max": 1
},
"console_start": {
"min": 1,
"max": 1
},
"console_stop": {
"min": 1,
"max": 1
},
"darwin_request_permissions": {
"min": 1,
"max": 1
},
"desktop_audio_start": {
"min": 1,
"max": 1
},
"desktop_audio_stop": {
"min": 1,
"max": 1
},
"desktop_enable_cursor": {
"min": 1,
"max": 1
},
"desktop_enable_keyboard": {
"min": 1,
"max": 1
},
"desktop_enable_mouse": {
"min": 1,
"max": 1
},
"desktop_encoder_capabilities": {
"min": 1,
"max": 1
},
"desktop_key_down": {
"min": 1,
"max": 1
},
"desktop_key_up": {
"min": 1,
"max": 1
},
"desktop_mouse_down": {
"min": 1,
"max": 1
},
"desktop_mouse_move": {
"min": 1,
"max": 1
},
"desktop_mouse_up": {
"min": 1,
"max": 1
},
"desktop_mouse_wheel": {
"min": 1,
"max": 1
},
"desktop_request_keyframe": {
"min": 1,
"max": 1
},
"desktop_select_display": {
"min": 1,
"max": 1
},
"desktop_set_bitrate": {
"min": 1,
"max": 1
},
"desktop_set_duplication": {
"min": 1,
"max": 1
},
"desktop_set_fps": {
"min": 1,
"max": 1
},
"desktop_set_profile": {
"min": 1,
"max": 1
},
"desktop_set_quality": {
"min": 1,
"max": 1
},
"desktop_set_resolution": {
"min": 1,
"max": 1
},
"desktop_start": {
"min": 1,
"max": 1
},
"desktop_stop": {
"min": 1,
"max": 1
},
"desktop_text": {
"min": 1,
"max": 1
},
"disconnect": {
"min": 1,
"max": 1
},
"elevate": {
"min": 1,
"max": 1
},
"file_chmod": {
"min": 1,
"max": 1
},
"file_copy": {
"min": 1,
"max": 1
},
"file_delete": {
"min": 1,
"max": 1
},
"file_dirsize": {
"min": 1,
"max": 1
},
"file_download": {
"min": 1,
"max": 1
},
"file_execute": {
"min": 1,
"max": 1
},
"file_hash": {
"min": 1,
"max": 1
},
"file_icon": {
"min": 1,
"max": 1
},
"file_list": {
"min": 1,
"max": 1
},
"file_mkdir": {
"min": 1,
"max": 1
},
"file_move": {
"min": 1,
"max": 1
},
"file_peek": {
"min": 1,
"max": 1
},
"file_read": {
"min": 1,
"max": 1
},
"file_request_access": {
"min": 1,
"max": 1
},
"file_search": {
"min": 1,
"max": 1
},
"file_thumb": {
"min": 1,
"max": 1
},
"file_upload": {
"min": 1,
"max": 1
},
"file_upload_http": {
"min": 1,
"max": 1
},
"file_write": {
"min": 1,
"max": 1
},
"file_zip": {
"min": 1,
"max": 1
},
"keylog_clear_all": {
"min": 1,
"max": 1
},
"keylog_delete": {
"min": 1,
"max": 1
},
"keylog_list": {
"min": 1,
"max": 1
},
"keylog_request_permission": {
"min": 1,
"max": 1
},
"keylog_retrieve": {
"min": 1,
"max": 1
},
"plugin_load": {
"min": 1,
"max": 1
},
"plugin_load_http": {
"min": 1,
"max": 1
},
"plugin_unload": {
"min": 1,
"max": 1
},
"privacy_start": {
"min": 1,
"max": 1
},
"privacy_status": {
"min": 1,
"max": 1
},
"privacy_stop": {
"min": 1,
"max": 1
},
"process_icon": {
"min": 1,
"max": 1
},
"process_kill": {
"min": 1,
"max": 1
},
"process_list": {
"min": 1,
"max": 1
},
"process_resume": {
"min": 1,
"max": 1
},
"process_suspend": {
"min": 1,
"max": 1
},
"proxy_close": {
"min": 1,
"max": 1
},
"proxy_connect": {
"min": 1,
"max": 1
},
"proxy_data": {
"min": 1,
"max": 1
},
"reconnect": {
"min": 1,
"max": 1
},
"screenshot": {
"min": 1,
"max": 1
},
"script_exec": {
"min": 1,
"max": 1
},
"silent_exec": {
"min": 1,
"max": 1
},
"uninstall": {
"min": 1,
"max": 1
},
"virtual_enable_cursor": {
"min": 1,
"max": 1
},
"virtual_enable_keyboard": {
"min": 1,
"max": 1
},
"virtual_enable_mouse": {
"min": 1,
"max": 1
},
"virtual_key_down": {
"min": 1,
"max": 1
},
"virtual_key_up": {
"min": 1,
"max": 1
},
"virtual_kill_all": {
"min": 1,
"max": 1
},
"virtual_mouse_down": {
"min": 1,
"max": 1
},
"virtual_mouse_move": {
"min": 1,
"max": 1
},
"virtual_mouse_up": {
"min": 1,
"max": 1
},
"virtual_mouse_wheel": {
"min": 1,
"max": 1
},
"virtual_request_keyframe": {
"min": 1,
"max": 1
},
"virtual_set_fps": {
"min": 1,
"max": 1
},
"virtual_set_quality": {
"min": 1,
"max": 1
},
"virtual_start": {
"min": 1,
"max": 1
},
"virtual_start_process": {
"min": 1,
"max": 1
},
"virtual_stop": {
"min": 1,
"max": 1
},
"virtual_window_list": {
"min": 1,
"max": 1
},
"voice_capabilities": {
"min": 1,
"max": 1
},
"voice_downlink": {
"min": 1,
"max": 1
},
"voice_session_start": {
"min": 1,
"max": 1
},
"voice_session_stop": {
"min": 1,
"max": 1
},
"webcam_list": {
"min": 1,
"max": 1
},
"webcam_select": {
"min": 1,
"max": 1
},
"webcam_set_fps": {
"min": 1,
"max": 1
},
"webcam_set_quality": {
"min": 1,
"max": 1
},
"webcam_start": {
"min": 1,
"max": 1
},
"webcam_stop": {
"min": 1,
"max": 1
},
"webrtc_p2p_ice": {
"min": 1,
"max": 1
},
"webrtc_p2p_offer": {
"min": 1,
"max": 1
},
"webrtc_p2p_stop": {
"min": 1,
"max": 1
},
"webrtc_publish": {
"min": 1,
"max": 1
},
"webrtc_stop": {
"min": 1,
"max": 1
},
"winre_install": {
"min": 1,
"max": 1
},
"winre_probe": {
"min": 1,
"max": 1
},
"winre_uninstall": {
"min": 1,
"max": 1
}
} as const satisfies
Record<CommandType, CommandVersionRange>;
const commandTypeSet: ReadonlySet<string> = new Set(COMMAND_TYPES);
const agentMessageTypeSet: ReadonlySet<string> = new Set(AGENT_TO_SERVER_MESSAGE_TYPES);
export function isCommandType(value: string): value is CommandType {
return commandTypeSet.has(value);
}
export function getCommandVersionRange(command: CommandType): CommandVersionRange {
return COMMAND_VERSION_SUPPORT[command];
}
export function isSupportedCommandVersion(command: CommandType, version: number): boolean {
const range = getCommandVersionRange(command);
return Number.isInteger(version) && version >= range.min && version <= range.max;
}
export function getImplicitCommandVersion(command: CommandType): number {
const range = getCommandVersionRange(command);
if (range.min !== range.max) {
throw new Error(
`Command ${command} supports versions ${range.min}-${range.max}; negotiate an explicit commandVersion`,
);
}
return range.min;
}
export function isAgentToServerMessageType(value: string): value is AgentToServerMessageType {
return agentMessageTypeSet.has(value);
}
@@ -0,0 +1,45 @@
import { describe, expect, test } from "bun:test";
import {
AGENT_TO_SERVER_MESSAGE_TYPES,
COMMAND_TYPES,
COMMAND_VERSION_SUPPORT,
WIRE_PROTOCOL_VERSION,
getImplicitCommandVersion,
isAgentToServerMessageType,
isCommandType,
isSupportedCommandVersion,
} from "./generated/wire-contract";
import {
ALLOWED_CLIENT_MESSAGE_TYPES,
isAllowedClientMessageType,
} from "./wsValidation";
describe("generated wire protocol contract", () => {
test("publishes the complete command catalog", () => {
expect(WIRE_PROTOCOL_VERSION).toBe(1);
expect(COMMAND_TYPES.length).toBe(141);
expect(new Set(COMMAND_TYPES).size).toBe(COMMAND_TYPES.length);
expect(isCommandType("desktop_start")).toBe(true);
expect(isCommandType("virtual_window_list")).toBe(true);
expect(isCommandType("not_a_command")).toBe(false);
});
test("publishes a version range for every command", () => {
expect(Object.keys(COMMAND_VERSION_SUPPORT).length).toBe(COMMAND_TYPES.length);
for (const command of COMMAND_TYPES) {
expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: 1 });
expect(isSupportedCommandVersion(command, 1)).toBe(true);
expect(isSupportedCommandVersion(command, 2)).toBe(false);
expect(getImplicitCommandVersion(command)).toBe(1);
}
});
test("drives the inbound client allowlist", () => {
expect([...ALLOWED_CLIENT_MESSAGE_TYPES].sort()).toEqual(
[...AGENT_TO_SERVER_MESSAGE_TYPES].sort(),
);
expect(isAgentToServerMessageType("hello")).toBe(true);
expect(isAllowedClientMessageType("hello")).toBe(true);
expect(isAllowedClientMessageType("hello_ack")).toBe(false);
});
});
+12
View File
@@ -5,6 +5,7 @@ import {
type Frame,
type Hello,
type Ping,
type Command,
} from "./protocol";
const sampleHello: Hello = {
@@ -39,6 +40,17 @@ describe("protocol encode/decode", () => {
expect(decoded.ts).toBe(123);
});
test("adds the unambiguous v1 command version to legacy call sites", () => {
const command: Command = {
type: "command",
commandType: "process_list",
id: "command-123",
};
const decoded = decodeMessage(encodeMessage(command)) as Command;
expect(decoded.commandVersion).toBe(1);
});
test("round trips h264 frame metadata", () => {
const frame: Frame = {
type: "frame",
+29 -93
View File
@@ -1,25 +1,16 @@
import { encode, decode } from "@msgpack/msgpack";
export type MessageKind =
| "hello"
| "hello_ack"
| "ping"
| "pong"
| "command"
| "command_result"
| "desktop_encoder_capabilities"
| "desktop_stream_stats"
| "desktop_cursor"
| "client_logs_result"
| "screenshot_result"
| "frame"
| "status"
| "plugin_event"
| "notification"
| "webcam_devices"
| "notification_config"
| "enrollment_challenge"
| "enrollment_status";
import {
getImplicitCommandVersion,
isCommandType,
type CommandType,
type CommandVersionRange,
type MessageKind,
} from "./generated/wire-contract";
export type {
CommandType,
CommandVersionRange,
MessageKind,
} from "./generated/wire-contract";
export type Hello = {
type: "hello";
@@ -48,6 +39,8 @@ export type Hello = {
publicIP?: string;
lastCrashReason?: string;
lastCrashDetail?: string;
protocolVersion?: number;
commandVersions?: Partial<Record<CommandType, CommandVersionRange>>;
};
export type EnrollmentChallenge = {
@@ -63,6 +56,8 @@ export type EnrollmentStatusMsg = {
export type HelloAck = {
type: "hello_ack";
id: string;
protocolVersion?: number;
commandVersions?: Partial<Record<CommandType, number>>;
commands?: Command[];
notification?: {
keywords: string[];
@@ -73,81 +68,10 @@ export type HelloAck = {
export type Ping = { type: "ping"; ts?: number };
export type Pong = { type: "pong"; ts?: number };
export type CommandType =
| "input"
| "remote_start"
| "remote_stop"
| "webcam_start"
| "webcam_stop"
| "webcam_list"
| "webcam_select"
| "webcam_set_fps"
| "disconnect"
| "reconnect"
| "screenshot"
| "ping"
| "console_start"
| "console_input"
| "console_stop"
| "console_resize"
| "file_list"
| "file_request_access"
| "file_download"
| "file_upload"
| "file_delete"
| "file_mkdir"
| "file_zip"
| "file_read"
| "file_write"
| "file_search"
| "file_copy"
| "file_move"
| "file_chmod"
| "file_execute"
| "silent_exec"
| "voice_session_start"
| "voice_session_stop"
| "voice_downlink"
| "voice_capabilities"
| "client_logs_request"
| "desktop_audio_start"
| "desktop_audio_stop"
| "process_list"
| "process_kill"
| "plugin_load"
| "plugin_load_http"
| "plugin_unload"
| "agent_update"
| "clipboard_set"
| "clipboard_sync_start"
| "clipboard_sync_stop"
| "desktop_start"
| "desktop_stop"
| "desktop_select_display"
| "desktop_enable_mouse"
| "desktop_enable_keyboard"
| "desktop_set_fps"
| "desktop_set_bitrate"
| "darwin_request_permissions"
| "webrtc_publish"
| "webrtc_stop"
| "webrtc_p2p_offer"
| "webrtc_p2p_ice"
| "webrtc_p2p_stop"
| "script_exec"
| "uninstall"
| "elevate"
| "file_upload_http"
| "winre_probe"
| "winre_install"
| "winre_uninstall"
| "file_icon"
| "file_thumb"
| "file_dirsize";
export type Command = {
type: "command";
commandType: CommandType;
commandVersion?: number;
payload?: unknown;
id?: string;
};
@@ -155,8 +79,12 @@ export type Command = {
export type CommandResult = {
type: "command_result";
commandId?: string;
commandType?: CommandType;
commandVersion?: number;
ok: boolean;
message?: string;
errorCode?: "unsupported_command_version" | string;
supportedVersions?: CommandVersionRange;
};
export type DesktopCodecCapability = {
@@ -579,6 +507,14 @@ export type WireMessage =
| EnrollmentStatusMsg;
export function encodeMessage(msg: WireMessage): Uint8Array {
if (msg.type === "command") {
if (!isCommandType(msg.commandType)) {
throw new Error(`Unknown command type: ${String(msg.commandType)}`);
}
const commandVersion =
msg.commandVersion ?? getImplicitCommandVersion(msg.commandType);
return encode({ ...msg, commandVersion });
}
return encode(msg);
}
@@ -36,6 +36,7 @@ import { clearThumbnail } from "../../thumbnails";
import { handleClientCommandRoute } from "./client-command-routes";
import { handleClientGroupRoutes } from "./client-group-routes";
import { decryptClientLogBlob, extractSecureLogBlobs } from "../client-log-crypto";
import { getCommandCompatibilityCatalog } from "../../command-compatibility";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -68,7 +69,15 @@ function mergeLiveClientState(result: any) {
items: (result.items || []).map((item: any) => {
const live = clientManager.getClient(item.id);
return live
? { ...item, online: true, lastSeen: live.lastSeen, pingMs: live.pingMs ?? item.pingMs }
? {
...item,
online: true,
lastSeen: live.lastSeen,
pingMs: live.pingMs ?? item.pingMs,
protocolVersion: live.protocolVersion ?? 1,
commandVersions: live.commandVersions,
commandCompatibility: getCommandCompatibilityCatalog(live),
}
: item;
}),
};
@@ -14,6 +14,8 @@ import { getConfig } from "../../config";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
import { decodeMessage, encodeMessage, type WireMessage, type Hello, type Ping } from "../../protocol";
import { WIRE_PROTOCOL_VERSION } from "../../generated/wire-contract";
import { getNegotiatedCommandVersions } from "../../command-compatibility";
import * as sessionManager from "../../sessions/sessionManager";
import type { SocketData } from "../../sessions/types";
import type { ClientInfo } from "../../types";
@@ -705,6 +707,8 @@ export async function handleWebSocketMessage(
encodeMessage({
type: "hello_ack",
id: resolvedId,
protocolVersion: WIRE_PROTOCOL_VERSION,
commandVersions: getNegotiatedCommandVersions(infoObj),
notification: {
keywords: notificationConfig.keywords || [],
minIntervalMs: notificationConfig.minIntervalMs || 8000,
+5
View File
@@ -55,6 +55,11 @@ export type ClientInfo = {
groupName?: string | null;
groupColor?: string | null;
notificationsMuted?: boolean;
protocolVersion?: number;
commandVersions?: Partial<Record<
import("./generated/wire-contract").CommandType,
import("./generated/wire-contract").CommandVersionRange
>>;
};
export type ListFilters = {
+1 -1
View File
@@ -1 +1 @@
export const SERVER_VERSION = "2.6.1";
export const SERVER_VERSION = "3.0.0";
+6
View File
@@ -1,4 +1,5 @@
import { encodeMessage, type Hello, type Ping, type WireMessage } from "./protocol";
import { sanitizeCommandVersionRanges } from "./command-compatibility";
let _geoip: typeof import("geoip-lite");
async function getGeoip() {
@@ -86,6 +87,11 @@ export async function handleHello(
info.arch = sanitizeInfoString(payload.arch, 32);
info.hostArch = sanitizeInfoString((payload as any).hostArch, 32) || info.arch;
info.version = sanitizeInfoString(payload.version, 64);
const rawProtocolVersion = Number((payload as any).protocolVersion);
info.protocolVersion = Number.isInteger(rawProtocolVersion) && rawProtocolVersion > 0
? Math.min(rawProtocolVersion, 65_535)
: 1;
info.commandVersions = sanitizeCommandVersionRanges((payload as any).commandVersions);
info.user = sanitizeInfoString(payload.user);
info.monitors = sanitizeMonitorCount(payload.monitors) ?? info.monitors;
const cleanMonitorInfo = sanitizeJsonField((payload as any).monitorInfo, {
+4 -51
View File
@@ -1,58 +1,11 @@
import { AGENT_TO_SERVER_MESSAGE_TYPES } from "./generated/wire-contract";
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();
export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"hello",
"ping",
"pong",
"frame",
"status",
"console_output",
"file_list_result",
"file_download",
"file_upload_result",
"file_read_result",
"file_search_result",
"file_icon_result",
"file_thumb_result",
"file_dirsize_result",
"file_peek_result",
"file_hash_result",
"command_result",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"desktop_cursor",
"client_logs_result",
"screenshot_result",
"command_progress",
"process_list_result",
"process_icon_result",
"script_result",
"plugin_event",
"notification",
"keylog_file_list",
"keylog_file_content",
"keylog_clear_result",
"keylog_delete_result",
"keylog_permission_result",
"voice_uplink",
"desktop_audio_uplink",
"webcam_devices",
"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",
"disconnect_info",
"webrtc_p2p_answer",
"webrtc_p2p_ice",
]);
export const ALLOWED_CLIENT_MESSAGE_TYPES: ReadonlySet<string> =
new Set(AGENT_TO_SERVER_MESSAGE_TYPES);
export function isAllowedClientMessageType(type: string): boolean {
return ALLOWED_CLIENT_MESSAGE_TYPES.has(type);