Skip to main content

ssh_commander_core/
desktop_protocol.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use tokio::sync::mpsc;
5use tokio_util::sync::CancellationToken;
6
7/// A decoded framebuffer update — a dirty rectangle with RGBA pixel data.
8///
9/// Emitted by the `DesktopProtocol::start_frame_loop` trait method. The RDP
10/// and VNC clients are currently stubs; once they produce real frames,
11/// `ConnectionManager::start_desktop_stream` will plumb these to the
12/// WebSocket server. Until then the struct and its field are "dead" only
13/// in the sense that no production code exercises them yet.
14#[allow(dead_code)]
15#[derive(Clone, Debug)]
16pub struct FrameUpdate {
17    pub x: u16,
18    pub y: u16,
19    pub width: u16,
20    pub height: u16,
21    /// Raw RGBA pixel data (width × height × 4 bytes)
22    pub rgba_data: Vec<u8>,
23}
24
25/// Unified trait for RDP and VNC remote desktop protocol clients.
26///
27/// Both `RdpClient` and `VncClient` implement this trait so that the
28/// `ConnectionManager` and Tauri commands can work protocol-agnostically.
29#[async_trait]
30pub trait DesktopProtocol: Send + Sync {
31    /// Start the frame update loop, sending `FrameUpdate` messages via the
32    /// provided sender until the cancellation token is triggered.
33    async fn start_frame_loop(
34        &self,
35        frame_tx: mpsc::UnboundedSender<FrameUpdate>,
36        cancel: CancellationToken,
37    ) -> Result<()>;
38
39    /// Send a keyboard event to the remote host.
40    async fn send_key(&self, key_code: u32, down: bool) -> Result<()>;
41
42    /// Send a pointer (mouse) event to the remote host.
43    async fn send_pointer(&self, x: u16, y: u16, button_mask: u8) -> Result<()>;
44
45    /// Request a full framebuffer update from the remote host.
46    async fn request_full_frame(&self) -> Result<()>;
47
48    /// Send clipboard text to the remote session.
49    async fn set_clipboard(&self, text: String) -> Result<()>;
50
51    /// Get the remote desktop dimensions (width, height).
52    fn desktop_size(&self) -> (u16, u16);
53
54    /// Request the remote desktop to resize to the given dimensions.
55    /// For RDP: sends a display resize request to the server.
56    /// For VNC: no-op (VNC does not support server-side resize; client-side scaling is used).
57    async fn resize(&mut self, width: u16, height: u16) -> Result<()>;
58
59    /// Disconnect and release resources.
60    async fn disconnect(&mut self) -> Result<()>;
61}
62
63// ---------------------------------------------------------------------------
64// Request / response data models shared between Tauri commands and WebSocket
65// ---------------------------------------------------------------------------
66
67/// Which remote-desktop protocol the client is asking for.
68///
69/// Serialised as `"RDP"` / `"VNC"`, while also accepting lowercase aliases for
70/// compatibility with older frontend payloads.
71#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
72pub enum DesktopKind {
73    #[serde(rename = "RDP", alias = "rdp")]
74    Rdp,
75    #[serde(rename = "VNC", alias = "vnc")]
76    Vnc,
77}
78
79/// Request to establish an RDP or VNC connection.
80#[derive(Deserialize)]
81pub struct DesktopConnectRequest {
82    pub connection_id: String,
83    pub protocol: DesktopKind,
84    pub host: String,
85    pub port: u16,
86    pub username: Option<String>, // RDP only
87    pub password: Option<String>,
88    pub domain: Option<String>, // RDP only
89    /// RDP resolution: "1024x768", "1280x720", "1920x1080", or "fit"
90    pub resolution: Option<String>,
91    /// VNC color depth: 24, 16, or 8
92    pub color_depth: Option<u8>,
93}
94
95impl std::fmt::Debug for DesktopConnectRequest {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        f.debug_struct("DesktopConnectRequest")
98            .field("connection_id", &self.connection_id)
99            .field("protocol", &format_args!("{:?}", self.protocol))
100            .field("host", &self.host)
101            .field("port", &self.port)
102            .field("username", &self.username)
103            .field(
104                "password",
105                &self
106                    .password
107                    .as_ref()
108                    .map(|_| "<redacted>")
109                    .unwrap_or("<none>"),
110            )
111            .field("domain", &self.domain)
112            .field("resolution", &self.resolution)
113            .field("color_depth", &self.color_depth)
114            .finish()
115    }
116}
117
118/// Response after a successful desktop connection.
119#[derive(Debug, Serialize)]
120pub struct DesktopConnectResponse {
121    pub width: u16,
122    pub height: u16,
123}
124
125// ---------------------------------------------------------------------------
126// Protocol-specific config structs (used internally by the clients)
127// ---------------------------------------------------------------------------
128
129#[derive(Clone)]
130pub struct RdpConfig {
131    pub host: String,
132    pub port: u16,
133    pub username: String,
134    /// Credential slot — consumed by the NLA handshake once `RdpClient::connect`
135    /// gets a real implementation (see rdp_client.rs).
136    #[allow(dead_code)]
137    pub password: String,
138    pub domain: Option<String>,
139    pub width: u16,
140    pub height: u16,
141}
142
143impl std::fmt::Debug for RdpConfig {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("RdpConfig")
146            .field("host", &self.host)
147            .field("port", &self.port)
148            .field("username", &self.username)
149            .field("password", &"<redacted>")
150            .field("domain", &self.domain)
151            .field("width", &self.width)
152            .field("height", &self.height)
153            .finish()
154    }
155}
156
157#[derive(Clone)]
158pub struct VncConfig {
159    pub host: String,
160    pub port: u16,
161    pub password: Option<String>,
162    pub color_depth: u8, // 24, 16, or 8
163}
164
165impl std::fmt::Debug for VncConfig {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_struct("VncConfig")
168            .field("host", &self.host)
169            .field("port", &self.port)
170            .field(
171                "password",
172                &self
173                    .password
174                    .as_ref()
175                    .map(|_| "<redacted>")
176                    .unwrap_or("<none>"),
177            )
178            .field("color_depth", &self.color_depth)
179            .finish()
180    }
181}
182
183impl DesktopConnectRequest {
184    /// Parse the resolution string into (width, height), defaulting to 1024×768.
185    pub fn parse_resolution(&self) -> (u16, u16) {
186        match self.resolution.as_deref() {
187            Some("1920x1080") => (1920, 1080),
188            Some("1280x720") => (1280, 720),
189            Some("1024x768") => (1024, 768),
190            _ => (1024, 768), // "fit" or unknown → default
191        }
192    }
193
194    /// Convert to an `RdpConfig`.
195    pub fn to_rdp_config(&self) -> RdpConfig {
196        let (w, h) = self.parse_resolution();
197        RdpConfig {
198            host: self.host.clone(),
199            port: self.port,
200            username: self.username.clone().unwrap_or_default(),
201            password: self.password.clone().unwrap_or_default(),
202            domain: self.domain.clone(),
203            width: w,
204            height: h,
205        }
206    }
207
208    /// Convert to a `VncConfig`.
209    pub fn to_vnc_config(&self) -> VncConfig {
210        VncConfig {
211            host: self.host.clone(),
212            port: self.port,
213            password: self.password.clone(),
214            color_depth: self.color_depth.unwrap_or(24),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn desktop_kind_accepts_uppercase_and_lowercase_wire_values() {
225        assert_eq!(
226            serde_json::from_str::<DesktopKind>("\"RDP\"").unwrap(),
227            DesktopKind::Rdp
228        );
229        assert_eq!(
230            serde_json::from_str::<DesktopKind>("\"rdp\"").unwrap(),
231            DesktopKind::Rdp
232        );
233        assert_eq!(
234            serde_json::from_str::<DesktopKind>("\"VNC\"").unwrap(),
235            DesktopKind::Vnc
236        );
237        assert_eq!(
238            serde_json::from_str::<DesktopKind>("\"vnc\"").unwrap(),
239            DesktopKind::Vnc
240        );
241    }
242}