Skip to main content

ssh_commander_core/
rdp_client.rs

1use crate::desktop_protocol::{DesktopProtocol, FrameUpdate, RdpConfig};
2use anyhow::Result;
3use async_trait::async_trait;
4use tokio::sync::mpsc;
5use tokio_util::sync::CancellationToken;
6
7/// RDP remote desktop client.
8///
9/// Uses the RDP protocol to connect to Windows hosts.  The actual protocol
10/// wire-work (TLS, NLA, graphics pipeline) will be implemented in a follow-up
11/// using the `ironrdp` crate ecosystem.  For now the struct compiles and
12/// exposes the full `DesktopProtocol` interface so that the rest of the
13/// application (commands, WebSocket, frontend) can be wired end-to-end.
14pub struct RdpClient {
15    config: RdpConfig,
16    desktop_width: u16,
17    desktop_height: u16,
18    connected: bool,
19}
20
21impl RdpClient {
22    /// Attempt to establish an RDP connection.
23    ///
24    /// Currently returns an error because the actual RDP protocol implementation
25    /// (ironrdp integration) is pending.  The function validates the config so
26    /// callers get a meaningful message.
27    pub async fn connect(config: &RdpConfig) -> Result<Self> {
28        if config.host.is_empty() {
29            return Err(anyhow::anyhow!("RDP host cannot be empty"));
30        }
31        if config.username.is_empty() {
32            return Err(anyhow::anyhow!(
33                "RDP username is required for NLA authentication"
34            ));
35        }
36
37        // TODO: implement actual RDP connection using ironrdp crate
38        // Steps would be:
39        // 1. TCP connect to host:port
40        // 2. TLS upgrade
41        // 3. NLA authentication (username, password, domain)
42        // 4. Negotiate display resolution
43        // 5. Start graphics pipeline
44        Err(anyhow::anyhow!(
45            "RDP protocol support is not yet implemented. \
46             Connection to {}:{} cannot be established.",
47            config.host,
48            config.port
49        ))
50    }
51
52    /// Create a client instance in disconnected state (for testing).
53    #[allow(dead_code)]
54    fn new_disconnected(config: RdpConfig) -> Self {
55        Self {
56            desktop_width: config.width,
57            desktop_height: config.height,
58            config,
59            connected: false,
60        }
61    }
62}
63
64#[async_trait]
65impl DesktopProtocol for RdpClient {
66    async fn start_frame_loop(
67        &self,
68        _frame_tx: mpsc::UnboundedSender<FrameUpdate>,
69        _cancel: CancellationToken,
70    ) -> Result<()> {
71        if !self.connected {
72            return Err(anyhow::anyhow!("RDP client is not connected"));
73        }
74        // TODO: decode incoming RDP framebuffer updates and send as FrameUpdate
75        Ok(())
76    }
77
78    async fn send_key(&self, _key_code: u32, _down: bool) -> Result<()> {
79        if !self.connected {
80            return Err(anyhow::anyhow!("RDP client is not connected"));
81        }
82        // TODO: forward as RDP scancode input event
83        Ok(())
84    }
85
86    async fn send_pointer(&self, _x: u16, _y: u16, _button_mask: u8) -> Result<()> {
87        if !self.connected {
88            return Err(anyhow::anyhow!("RDP client is not connected"));
89        }
90        // TODO: forward as RDP pointer input event
91        Ok(())
92    }
93
94    async fn request_full_frame(&self) -> Result<()> {
95        if !self.connected {
96            return Err(anyhow::anyhow!("RDP client is not connected"));
97        }
98        // TODO: request full framebuffer refresh
99        Ok(())
100    }
101
102    async fn set_clipboard(&self, _text: String) -> Result<()> {
103        if !self.connected {
104            return Err(anyhow::anyhow!("RDP client is not connected"));
105        }
106        // TODO: send via CLIPRDR virtual channel
107        Ok(())
108    }
109
110    fn desktop_size(&self) -> (u16, u16) {
111        (self.desktop_width, self.desktop_height)
112    }
113
114    async fn resize(&mut self, width: u16, height: u16) -> Result<()> {
115        if !self.connected {
116            return Err(anyhow::anyhow!("RDP client is not connected"));
117        }
118        // TODO: send RDP display resize PDU to server
119        // If server rejects, retain current resolution
120        self.desktop_width = width;
121        self.desktop_height = height;
122        Ok(())
123    }
124
125    async fn disconnect(&mut self) -> Result<()> {
126        if self.connected {
127            // TODO: send graceful RDP disconnect request
128            self.connected = false;
129            tracing::info!(
130                "RDP disconnected from {}:{}",
131                self.config.host,
132                self.config.port
133            );
134        }
135        Ok(())
136    }
137}