helios-remote/crates/common/src/protocol.rs

218 lines
6.7 KiB
Rust

use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Information about a single window on the client machine.
/// `label` is a human-readable, lowercase identifier (e.g. "google_chrome", "discord").
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowInfo {
pub id: u64,
pub title: String,
pub label: String,
pub visible: bool,
}
/// Validate a device/window label: lowercase, no whitespace, only a-z 0-9 - _
pub fn is_valid_label(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
}
/// Convert an arbitrary string into a valid label.
/// Lowercase, replace whitespace and invalid chars with '_', collapse runs.
pub fn sanitize_label(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut prev_underscore = false;
for c in s.chars() {
if c.is_ascii_alphanumeric() {
result.push(c.to_ascii_lowercase());
prev_underscore = false;
} else if c == '-' {
result.push('-');
prev_underscore = false;
} else {
// Replace whitespace and other chars with _
if !prev_underscore && !result.is_empty() {
result.push('_');
prev_underscore = true;
}
}
}
// Trim trailing _
result.trim_end_matches('_').to_string()
}
/// Messages sent from the relay server to a connected client
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
/// Request a full-screen screenshot
ScreenshotRequest { request_id: Uuid },
/// Capture a specific window by its HWND
WindowScreenshotRequest { request_id: Uuid, window_id: u64 },
/// Fetch the last N lines of the client log file
LogsRequest { request_id: Uuid, lines: u32 },
/// Show a MessageBox on the client asking the user to do something
PromptRequest {
request_id: Uuid,
message: String,
title: Option<String>,
},
/// Show a non-blocking notification to the user (fire-and-forget)
InformRequest {
request_id: Uuid,
message: String,
title: Option<String>,
},
/// Execute a shell command on the client
ExecRequest {
request_id: Uuid,
command: String,
timeout_ms: Option<u64>,
},
/// Acknowledge a client message
Ack { request_id: Uuid },
/// Server-side error response
Error {
request_id: Option<Uuid>,
message: String,
},
/// List all visible windows on the client
ListWindowsRequest { request_id: Uuid },
/// Minimize all windows (like Win+D)
MinimizeAllRequest { request_id: Uuid },
/// Bring a window to the foreground
FocusWindowRequest { request_id: Uuid, window_id: u64 },
/// Maximize a window and bring it to the foreground
MaximizeAndFocusRequest { request_id: Uuid, window_id: u64 },
/// Request client version info
VersionRequest { request_id: Uuid },
/// Upload a file to the client
UploadRequest {
request_id: Uuid,
path: String,
content_base64: String,
},
/// Download a file from the client
DownloadRequest {
request_id: Uuid,
path: String,
},
/// Launch a program on the client (fire-and-forget)
RunRequest {
request_id: Uuid,
program: String,
args: Vec<String>,
},
/// Get the contents of the client's clipboard
ClipboardGetRequest { request_id: Uuid },
/// Set the contents of the client's clipboard
ClipboardSetRequest { request_id: Uuid, text: String },
/// Request client to self-update and restart
UpdateRequest { request_id: Uuid },
}
/// Messages sent from the client to the relay server
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
/// Client registers itself with its device label
Hello { label: String },
/// Response to a screenshot request — base64-encoded PNG
ScreenshotResponse {
request_id: Uuid,
image_base64: String,
width: u32,
height: u32,
},
/// Response to an exec request
ExecResponse {
request_id: Uuid,
stdout: String,
stderr: String,
exit_code: i32,
},
/// Generic acknowledgement
Ack { request_id: Uuid },
/// Client error response
Error {
request_id: Uuid,
message: String,
},
/// Response to a list-windows request
ListWindowsResponse {
request_id: Uuid,
windows: Vec<WindowInfo>,
},
/// Response to a version request
VersionResponse {
request_id: Uuid,
version: String,
commit: String,
},
LogsResponse {
request_id: Uuid,
content: String,
log_path: String,
},
/// Response to a download request
DownloadResponse {
request_id: Uuid,
content_base64: String,
size: u64,
},
/// Response to a clipboard-get request
ClipboardGetResponse { request_id: Uuid, text: String },
/// Response to a prompt request
PromptResponse { request_id: Uuid, answer: String },
/// Response to an update request
UpdateResponse {
request_id: Uuid,
success: bool,
message: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_labels() {
assert!(is_valid_label("moritz_pc"));
assert!(is_valid_label("my-desktop"));
assert!(is_valid_label("pc01"));
assert!(!is_valid_label("Moritz PC"));
assert!(!is_valid_label(""));
assert!(!is_valid_label("has spaces"));
assert!(!is_valid_label("UPPER"));
}
#[test]
fn test_sanitize_label() {
assert_eq!(sanitize_label("Moritz PC"), "moritz_pc");
assert_eq!(sanitize_label("My Desktop!!"), "my_desktop");
assert_eq!(sanitize_label("hello-world"), "hello-world");
assert_eq!(sanitize_label("DESKTOP-ABC123"), "desktop-abc123");
}
#[test]
fn test_server_message_serialization() {
let msg = ServerMessage::ExecRequest {
request_id: Uuid::nil(),
command: "echo hello".into(),
timeout_ms: None,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("exec_request"));
assert!(json.contains("echo hello"));
}
#[test]
fn test_client_message_serialization() {
let msg = ClientMessage::Hello { label: "test-pc".into() };
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("hello"));
assert!(json.contains("test-pc"));
}
}