218 lines
6.7 KiB
Rust
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"));
|
|
}
|
|
}
|