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)] pub 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] use crate::pdu::FormatList;
25
26pub 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#[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; 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 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 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 Ok(Vec::new())
140 }
141
142 fn handle_monitor_ready(&mut self) -> PduResult<Vec<SvcMessage>> {
143 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 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 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 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 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 pub fn initiate_paste(&self, requested_format: ClipboardFormatId) -> PduResult<CliprdrSvcMessages<R>> {
269 ready_guard!(self, initiate_paste);
270
271 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 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 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 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}