Initial implementation: relay server + common protocol + client stub

This commit is contained in:
Helios 2026-03-02 18:03:46 +01:00
commit 7285a33cff
No known key found for this signature in database
GPG key ID: C8259547CD8309B5
17 changed files with 926 additions and 0 deletions

9
crates/common/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "helios-common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] }

View file

@ -0,0 +1,35 @@
use std::fmt;
#[derive(Debug)]
pub enum HeliosError {
/// WebSocket protocol error
Protocol(String),
/// JSON serialization/deserialization error
Serialization(String),
/// Session not found
SessionNotFound(String),
/// Request timed out waiting for client response
Timeout(String),
/// Generic internal error
Internal(String),
}
impl fmt::Display for HeliosError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HeliosError::Protocol(msg) => write!(f, "Protocol error: {msg}"),
HeliosError::Serialization(msg) => write!(f, "Serialization error: {msg}"),
HeliosError::SessionNotFound(id) => write!(f, "Session not found: {id}"),
HeliosError::Timeout(msg) => write!(f, "Request timed out: {msg}"),
HeliosError::Internal(msg) => write!(f, "Internal error: {msg}"),
}
}
}
impl std::error::Error for HeliosError {}
impl From<serde_json::Error> for HeliosError {
fn from(e: serde_json::Error) -> Self {
HeliosError::Serialization(e.to_string())
}
}

5
crates/common/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod protocol;
pub mod error;
pub use protocol::*;
pub use error::*;

View file

@ -0,0 +1,118 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// 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 screenshot from the client
ScreenshotRequest { request_id: Uuid },
/// Execute a shell command on the client
ExecRequest {
request_id: Uuid,
command: String,
},
/// Simulate a mouse click
ClickRequest {
request_id: Uuid,
x: i32,
y: i32,
button: MouseButton,
},
/// Type text on the client
TypeRequest {
request_id: Uuid,
text: String,
},
/// Acknowledge a client message
Ack { request_id: Uuid },
/// Server-side error response
Error {
request_id: Option<Uuid>,
message: String,
},
}
/// 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 optional display name
Hello { label: Option<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 for click/type
Ack { request_id: Uuid },
/// Client error response
Error {
request_id: Uuid,
message: String,
},
}
/// Mouse button variants
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MouseButton {
Left,
Right,
Middle,
}
impl Default for MouseButton {
fn default() -> Self {
MouseButton::Left
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_message_serialization() {
let msg = ServerMessage::ExecRequest {
request_id: Uuid::nil(),
command: "echo hello".into(),
};
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: Some("test-pc".into()) };
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("hello"));
assert!(json.contains("test-pc"));
}
#[test]
fn test_roundtrip() {
let msg = ClientMessage::ExecResponse {
request_id: Uuid::nil(),
stdout: "hello\n".into(),
stderr: String::new(),
exit_code: 0,
};
let json = serde_json::to_string(&msg).unwrap();
let decoded: ClientMessage = serde_json::from_str(&json).unwrap();
match decoded {
ClientMessage::ExecResponse { exit_code, .. } => assert_eq!(exit_code, 0),
_ => panic!("wrong variant"),
}
}
}