ironrdp_cliprdr/
lib.rs

1#![cfg_attr(doc, doc = include_str!("../README.md"))]
2#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
3#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
4
5pub mod backend;
6pub mod pdu;
7
8use backend::CliprdrBackend;
9use ironrdp_core::{decode, AsAny, EncodeResult};
10use ironrdp_pdu::gcc::ChannelName;
11use ironrdp_pdu::{decode_err, encode_err, PduResult};
12use ironrdp_svc::{
13    ChannelFlags, CompressionCondition, SvcClientProcessor, SvcMessage, SvcProcessor, SvcProcessorMessages,
14    SvcServerProcessor,
15};
16use pdu::{
17    Capabilities, ClientTemporaryDirectory, ClipboardFormat, ClipboardFormatId, ClipboardGeneralCapabilityFlags,
18    ClipboardPdu, ClipboardProtocolVersion, FileContentsResponse, FormatDataRequest, FormatListResponse,
19    OwnedFormatDataResponse,
20};
21use tracing::{error, info};
22
23#[rustfmt::skip] // do not reorder
24use crate::pdu::FormatList;
25
26/// PDUs for sending to the server on the CLIPRDR channel.
27pub type CliprdrSvcMessages<R> = SvcProcessorMessages<Cliprdr<R>>;
28
29#[derive(Debug)]
30enum ClipboardError {
31    FormatListRejected,
32}
33
34impl core::fmt::Display for ClipboardError {
35    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        match self {
37            ClipboardError::FormatListRejected => write!(f, "sent format list was rejected"),
38        }
39    }
40}
41
42#[derive(Debug, Copy, Clone, PartialEq, Eq)]
43enum CliprdrState {
44    Initialization,
45    Ready,
46    Failed,
47}
48
49pub trait Role: core::fmt::Debug + Send + 'static {
50    fn is_server() -> bool;
51}
52
53/// CLIPRDR static virtual channel endpoint implementation
54#[derive(Debug)]
55pub struct Cliprdr<R: Role> {
56    backend: Box<dyn CliprdrBackend>,
57    capabilities: Capabilities,
58    state: CliprdrState,
59    _marker: core::marker::PhantomData<R>,
60}
61
62pub type CliprdrClient = Cliprdr<Client>;
63pub type CliprdrServer = Cliprdr<Server>;
64
65impl SvcClientProcessor for CliprdrClient {}
66impl SvcServerProcessor for CliprdrServer {}
67
68impl<R: Role> AsAny for Cliprdr<R> {
69    #[inline]
70    fn as_any(&self) -> &dyn core::any::Any {
71        self
72    }
73
74    #[inline]
75    fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
76        self
77    }
78}
79
80macro_rules! ready_guard {
81        ($self:ident, $function:ident) => {{
82            let _ = Self::$function; // ensure the function actually exists
83
84            if $self.state != CliprdrState::Ready {
85                error!(?$self.state, concat!("Attempted to initiate ", stringify!($function), " in incorrect state"));
86                return Ok(Vec::new().into());
87            }
88        }};
89    }
90
91impl<R: Role> Cliprdr<R> {
92    const CHANNEL_NAME: ChannelName = ChannelName::from_static(b"cliprdr\0");
93
94    pub fn new(backend: Box<dyn CliprdrBackend>) -> Self {
95        // This CLIPRDR implementation supports long format names by default
96        let flags = ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES | backend.client_capabilities();
97
98        Self {
99            backend,
100            state: CliprdrState::Initialization,
101            capabilities: Capabilities::new(ClipboardProtocolVersion::V2, flags),
102            _marker: core::marker::PhantomData,
103        }
104    }
105
106    pub fn downcast_backend<T: CliprdrBackend>(&self) -> Option<&T> {
107        self.backend.as_any().downcast_ref::<T>()
108    }
109
110    pub fn downcast_backend_mut<T: CliprdrBackend>(&mut self) -> Option<&mut T> {
111        self.backend.as_any_mut().downcast_mut::<T>()
112    }
113
114    fn are_long_format_names_enabled(&self) -> bool {
115        self.capabilities
116            .flags()
117            .contains(ClipboardGeneralCapabilityFlags::USE_LONG_FORMAT_NAMES)
118    }
119
120    fn build_format_list(&self, formats: &[ClipboardFormat]) -> EncodeResult<FormatList<'static>> {
121        FormatList::new_unicode(formats, self.are_long_format_names_enabled())
122    }
123
124    fn handle_error_transition(&mut self, err: ClipboardError) -> PduResult<Vec<SvcMessage>> {
125        // Failure of clipboard is not an critical error, but we should properly report it
126        // and transition channel to failed state.
127        self.state = CliprdrState::Failed;
128        error!("CLIPRDR(clipboard) failed: {err}");
129
130        Ok(Vec::new())
131    }
132
133    fn handle_server_capabilities(&mut self, server_capabilities: Capabilities) -> PduResult<Vec<SvcMessage>> {
134        self.capabilities.downgrade(&server_capabilities);
135        self.backend
136            .on_process_negotiated_capabilities(self.capabilities.flags());
137
138        // Do not send anything, wait for monitor ready pdu
139        Ok(Vec::new())
140    }
141
142    fn handle_monitor_ready(&mut self) -> PduResult<Vec<SvcMessage>> {
143        // Request client to sent list of initially available formats and wait for the backend
144        // response.
145        self.backend.on_request_format_list();
146        Ok(Vec::new())
147    }
148
149    fn handle_format_list_response(&mut self, response: FormatListResponse) -> PduResult<Vec<SvcMessage>> {
150        match response {
151            FormatListResponse::Ok => {
152                if !R::is_server() {
153                    if self.state == CliprdrState::Initialization {
154                        info!("CLIPRDR(clipboard) virtual channel has been initialized");
155                        self.state = CliprdrState::Ready;
156                        self.backend.on_ready();
157                    } else {
158                        info!("CLIPRDR(clipboard) Remote has received format list successfully");
159                    }
160                }
161            }
162            FormatListResponse::Fail => {
163                return self.handle_error_transition(ClipboardError::FormatListRejected);
164            }
165        }
166
167        Ok(Vec::new())
168    }
169
170    fn handle_format_list(&mut self, format_list: FormatList<'_>) -> PduResult<Vec<SvcMessage>> {
171        if R::is_server() && self.state == CliprdrState::Initialization {
172            info!("CLIPRDR(clipboard) virtual channel has been initialized");
173            self.state = CliprdrState::Ready;
174            self.backend.on_ready();
175        }
176
177        let formats = format_list.get_formats(self.are_long_format_names_enabled())?;
178        self.backend.on_remote_copy(&formats);
179
180        let pdu = ClipboardPdu::FormatListResponse(FormatListResponse::Ok);
181
182        Ok(vec![into_cliprdr_message(pdu)])
183    }
184
185    /// Submits the format data response, returning a [`CliprdrSvcMessages`] to send on the channel.
186    ///
187    /// Should be called by the clipboard implementation when it receives data from the OS clipboard
188    /// and is ready to sent it to the server. This should happen after
189    /// [`CliprdrBackend::on_format_data_request`] is called by [`Cliprdr`].
190    ///
191    /// If data is not available anymore, an error response should be sent instead.
192    pub fn submit_format_data(&self, response: OwnedFormatDataResponse) -> PduResult<CliprdrSvcMessages<R>> {
193        ready_guard!(self, submit_format_data);
194
195        let pdu = ClipboardPdu::FormatDataResponse(response);
196
197        Ok(vec![into_cliprdr_message(pdu)].into())
198    }
199
200    /// Submits the file contents response, returning a [`CliprdrSvcMessages`] to send on the channel.
201    ///
202    /// Should be called by the clipboard implementation when file data is ready to sent it to the
203    /// server. This should happen after [`CliprdrBackend::on_file_contents_request`] is called
204    /// by [`Cliprdr`].
205    ///
206    /// If data is not available anymore, an error response should be sent instead.
207    pub fn submit_file_contents(&self, response: FileContentsResponse<'static>) -> PduResult<CliprdrSvcMessages<R>> {
208        ready_guard!(self, submit_file_contents);
209
210        let pdu = ClipboardPdu::FileContentsResponse(response);
211
212        Ok(vec![into_cliprdr_message(pdu)].into())
213    }
214
215    pub fn capabilities(&self) -> PduResult<SvcMessage> {
216        let pdu = ClipboardPdu::Capabilities(self.capabilities.clone());
217
218        Ok(into_cliprdr_message(pdu))
219    }
220
221    pub fn monitor_ready(&self) -> PduResult<SvcMessage> {
222        let pdu = ClipboardPdu::MonitorReady;
223
224        Ok(into_cliprdr_message(pdu))
225    }
226
227    /// Starts processing of `CLIPRDR` copy command. Should be called by the clipboard
228    /// implementation when user performs OS-specific copy command (e.g. `Ctrl+C` shortcut on
229    /// keyboard)
230    pub fn initiate_copy(&self, available_formats: &[ClipboardFormat]) -> PduResult<CliprdrSvcMessages<R>> {
231        let mut pdus = Vec::new();
232
233        if R::is_server() {
234            pdus.push(ClipboardPdu::FormatList(
235                self.build_format_list(available_formats).map_err(|e| encode_err!(e))?,
236            ));
237        } else {
238            match self.state {
239                CliprdrState::Ready => {
240                    pdus.push(ClipboardPdu::FormatList(
241                        self.build_format_list(available_formats).map_err(|e| encode_err!(e))?,
242                    ));
243                }
244                CliprdrState::Initialization => {
245                    // During initialization state, first copy action is synthetic and should be sent along with
246                    // capabilities and temporary directory PDUs.
247                    pdus.push(ClipboardPdu::Capabilities(self.capabilities.clone()));
248                    pdus.push(ClipboardPdu::TemporaryDirectory(
249                        ClientTemporaryDirectory::new(self.backend.temporary_directory())
250                            .map_err(|e| encode_err!(e))?,
251                    ));
252                    pdus.push(ClipboardPdu::FormatList(
253                        self.build_format_list(available_formats).map_err(|e| encode_err!(e))?,
254                    ));
255                }
256                _ => {
257                    error!(?self.state, "Attempted to initiate copy in incorrect state");
258                }
259            }
260        }
261
262        Ok(pdus.into_iter().map(into_cliprdr_message).collect::<Vec<_>>().into())
263    }
264
265    /// Starts processing of `CLIPRDR` paste command. Should be called by the clipboard
266    /// implementation when user performs OS-specific paste command (e.g. `Ctrl+V` shortcut on
267    /// keyboard)
268    pub fn initiate_paste(&self, requested_format: ClipboardFormatId) -> PduResult<CliprdrSvcMessages<R>> {
269        ready_guard!(self, initiate_paste);
270
271        // When user initiates paste, we should send format data request to server, and expect to
272        // receive response with contents via `FormatDataResponse` PDU.
273        let pdu = ClipboardPdu::FormatDataRequest(FormatDataRequest {
274            format: requested_format,
275        });
276
277        Ok(vec![into_cliprdr_message(pdu)].into())
278    }
279}
280
281impl<R: Role> SvcProcessor for Cliprdr<R> {
282    fn channel_name(&self) -> ChannelName {
283        Self::CHANNEL_NAME
284    }
285
286    fn start(&mut self) -> PduResult<Vec<SvcMessage>> {
287        if self.state != CliprdrState::Initialization {
288            error!("Attempted to start clipboard static virtual channel in invalid state");
289        }
290
291        if R::is_server() {
292            Ok(vec![self.capabilities()?, self.monitor_ready()?])
293        } else {
294            Ok(Vec::new())
295        }
296    }
297
298    fn process(&mut self, payload: &[u8]) -> PduResult<Vec<SvcMessage>> {
299        let pdu = decode::<ClipboardPdu<'_>>(payload).map_err(|e| decode_err!(e))?;
300
301        if self.state == CliprdrState::Failed {
302            error!("Attempted to process clipboard static virtual channel in failed state");
303            return Ok(Vec::new());
304        }
305
306        match pdu {
307            ClipboardPdu::Capabilities(caps) => self.handle_server_capabilities(caps),
308            ClipboardPdu::FormatList(format_list) => self.handle_format_list(format_list),
309            ClipboardPdu::FormatListResponse(response) => self.handle_format_list_response(response),
310            ClipboardPdu::MonitorReady => self.handle_monitor_ready(),
311            ClipboardPdu::LockData(id) => {
312                self.backend.on_lock(id);
313                Ok(Vec::new())
314            }
315            ClipboardPdu::UnlockData(id) => {
316                self.backend.on_unlock(id);
317                Ok(Vec::new())
318            }
319            ClipboardPdu::FormatDataRequest(request) => {
320                self.backend.on_format_data_request(request);
321
322                // NOTE: An actual data should be sent later via `submit_format_data` method,
323                // therefore we do not send anything immediately.
324                Ok(Vec::new())
325            }
326            ClipboardPdu::FormatDataResponse(response) => {
327                self.backend.on_format_data_response(response);
328                Ok(Vec::new())
329            }
330            ClipboardPdu::FileContentsRequest(request) => {
331                self.backend.on_file_contents_request(request);
332                Ok(Vec::new())
333            }
334            ClipboardPdu::FileContentsResponse(response) => {
335                self.backend.on_file_contents_response(response);
336                Ok(Vec::new())
337            }
338            ClipboardPdu::TemporaryDirectory(_) => {
339                // do nothing
340                Ok(Vec::new())
341            }
342        }
343    }
344
345    fn compression_condition(&self) -> CompressionCondition {
346        CompressionCondition::WhenRdpDataIsCompressed
347    }
348}
349
350fn into_cliprdr_message(pdu: ClipboardPdu<'static>) -> SvcMessage {
351    // Adding [`CHANNEL_FLAG_SHOW_PROTOCOL`] is a must for clipboard svc messages, because they
352    // contain chunked data. This is the requirement from `MS_RDPBCGR` specification.
353    SvcMessage::from(pdu).with_flags(ChannelFlags::SHOW_PROTOCOL)
354}
355
356#[derive(Debug)]
357pub struct Client {}
358
359impl Role for Client {
360    fn is_server() -> bool {
361        false
362    }
363}
364
365#[derive(Debug)]
366pub struct Server {}
367
368impl Role for Server {
369    fn is_server() -> bool {
370        true
371    }
372}