feat: clipboard support for web client (#259)

This commit is contained in:
Vladyslav Nikonov
2023-11-08 13:57:21 +02:00
committed by GitHub
parent c666dc5f03
commit 2b501496d9
30 changed files with 2319 additions and 33 deletions
Generated
+12
View File
@@ -1763,6 +1763,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "ironrdp-cliprdr-format"
version = "0.1.0"
dependencies = [
"ironrdp-pdu",
"png",
"thiserror",
]
[[package]]
name = "ironrdp-cliprdr-native"
version = "0.1.0"
@@ -1816,6 +1825,7 @@ version = "0.0.0"
dependencies = [
"arbitrary",
"ironrdp-cliprdr",
"ironrdp-cliprdr-format",
"ironrdp-graphics",
"ironrdp-pdu",
"ironrdp-rdpdr",
@@ -1952,6 +1962,7 @@ dependencies = [
"expect-test",
"hex",
"ironrdp-cliprdr",
"ironrdp-cliprdr-format",
"ironrdp-connector",
"ironrdp-fuzzing",
"ironrdp-graphics",
@@ -1999,6 +2010,7 @@ dependencies = [
"gloo-net",
"gloo-timers",
"ironrdp",
"ironrdp-cliprdr-format",
"ironrdp-futures",
"ironrdp-rdcleanpath",
"js-sys",
+1
View File
@@ -27,6 +27,7 @@ ironrdp-async = { version = "0.1", path = "crates/ironrdp-async" }
ironrdp-blocking = { version = "0.1", path = "crates/ironrdp-blocking" }
ironrdp-cliprdr = { version = "0.1", path = "crates/ironrdp-cliprdr" }
ironrdp-cliprdr-native = { version = "0.1", path = "crates/ironrdp-cliprdr-native" }
ironrdp-cliprdr-format = { version = "0.1", path = "crates/ironrdp-cliprdr-format" }
ironrdp-connector = { version = "0.1", path = "crates/ironrdp-connector" }
ironrdp-dvc = { version = "0.1", path = "crates/ironrdp-dvc" }
ironrdp-error = { version = "0.1", path = "crates/ironrdp-error" }
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "ironrdp-cliprdr-format"
version = "0.1.0"
readme = "README.md"
description = "CLIPRDR format conversion library"
edition.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[dependencies]
ironrdp-pdu.workspace = true
thiserror.workspace = true
png = "0.17"
+7
View File
@@ -0,0 +1,7 @@
# IronRDP CLIPRDR formats decoding/encoding library
This Library provides the conversion logic between RDP-specific clipboard formats and
widely used formats like PNG for images, plain string for HTML etc.
### INVARIANTS
- This crate expects the target machine's pointer size (usize) to be equal or greater than 32bits
File diff suppressed because it is too large Load Diff
+149
View File
@@ -0,0 +1,149 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum HtmlError {
#[error("Invalid CF_HTML format")]
InvalidFormat,
#[error("Invalid UTF-8")]
InvalidUtf8(#[from] std::string::FromUtf8Error),
#[error("Failed to parse integer")]
InvalidInteger(#[from] std::num::ParseIntError),
#[error("Invalid integer conversion")]
InvalidConversion,
}
/// Convert `CF_HTML` format to plain text.
pub fn cf_html_to_text(input: &[u8]) -> Result<String, HtmlError> {
let mut start_fragment = None;
let mut end_fragment = None;
let mut headers_cursor = input;
let fragment = loop {
// Line split logic is manual instead of using BufReader::read_line because
// the line ending could be represented as `\r\n`, `\n` or even `\r`.
const ENDLINE_CONTROLS: &[u8] = &[b'\r', b'\n'];
// Failed to find the end of the line
let end_pos = match headers_cursor.iter().position(|ch| ENDLINE_CONTROLS.contains(ch)) {
Some(pos) => pos,
None => return Err(HtmlError::InvalidFormat),
};
let line = String::from_utf8(headers_cursor[..end_pos].to_vec())?;
let header_value_to_u32 = |value: &str| value.trim_start_matches('0').parse::<u32>();
match line.split_once(':') {
Some((key, value)) => match key {
"StartFragment" => {
start_fragment = Some(header_value_to_u32(value)?);
}
"EndFragment" => {
end_fragment = Some(header_value_to_u32(value)?);
}
_ => {
// We are not interested in other headers.
}
},
None => {
if start_fragment.is_none() || end_fragment.is_none() {
// We reached the end of the headers, but we didn't find the required ones,
// so the format is invalid.
return Err(HtmlError::InvalidFormat);
}
}
};
if let (Some(start), Some(end)) = (start_fragment, end_fragment) {
let start = usize::try_from(start).map_err(|_| HtmlError::InvalidConversion)?;
let end = usize::try_from(end).map_err(|_| HtmlError::InvalidConversion)?;
// Extract fragment from the original buffer.
if start > end || end > input.len() {
return Err(HtmlError::InvalidFormat);
}
break String::from_utf8(input[start..end].to_vec())?;
}
// INVARIANT: end_pos < headers_cursor.len() - 1
// This is safe because we already checked above that the line ends with `\r` or `\n`.
#[allow(clippy::arithmetic_side_effects)]
{
// Go to the next line, skipping any leftover `LF` if CRLF was used.
let has_leftover_lf = end_pos + 1 != headers_cursor.len()
&& headers_cursor[end_pos] == b'\r'
&& headers_cursor[end_pos + 1] == b'\n';
if has_leftover_lf {
headers_cursor = &headers_cursor[end_pos + 2..];
} else {
headers_cursor = &headers_cursor[end_pos + 1..];
}
}
};
Ok(fragment)
}
/// Convert plain text HTML to `CF_HTML` format.
pub fn text_to_cf_html(fragment: &str) -> Vec<u8> {
let mut buffer = Vec::new();
// INVARIANT: key.len() + value.len() + ":\r\n".len() < usize::MAX
// This is always true because we know `key` and `value` used in code below are
// short and their sizes are far from `usize::MAX`.
#[allow(clippy::arithmetic_side_effects)]
let mut write_header = |key: &str, value: &str| {
let size = key.len() + value.len() + ":\r\n".len();
buffer.reserve(size);
buffer.extend_from_slice(key.as_bytes());
buffer.extend_from_slice(b":");
let value_pos = buffer.len();
buffer.extend_from_slice(value.as_bytes());
buffer.extend_from_slice(b"\r\n");
value_pos
};
const POS_PLACEHOLDER: &str = "0000000000";
write_header("Version", "0.9");
let start_html_placeholder_pos = write_header("StartHTML", POS_PLACEHOLDER);
let end_html_placeholder_pos = write_header("EndHTML", POS_PLACEHOLDER);
let start_fragment_placeholder_pos = write_header("StartFragment", POS_PLACEHOLDER);
let end_fragment_placeholder_pos = write_header("EndFragment", POS_PLACEHOLDER);
let start_html_pos = buffer.len();
buffer.extend_from_slice(b"<html>\r\n<body>\r\n<!--StartFragment-->");
let start_fragment_pos = buffer.len();
buffer.extend_from_slice(fragment.as_bytes());
let end_fragment_pos = buffer.len();
buffer.extend_from_slice(b"<!--EndFragment-->\r\n</body>\r\n</html>");
let end_html_pos = buffer.len();
let start_html_pos_value = format!("{:0>10}", start_html_pos);
let end_html_pos_value = format!("{:0>10}", end_html_pos);
let start_fragment_pos_value = format!("{:0>10}", start_fragment_pos);
let end_fragment_pos_value = format!("{:0>10}", end_fragment_pos);
// INVARIANT: placeholder_pos + POS_PLACEHOLDER.len() < buffer.len()
// This is always valid because we know that placeholder is always present in the buffer
// fter the header is written and placeholder is within the bounds of the buffer.
#[allow(clippy::arithmetic_side_effects)]
let mut replace_placeholder = |placeholder_pos: usize, placeholder_value: &str| {
buffer[placeholder_pos..placeholder_pos + POS_PLACEHOLDER.len()].copy_from_slice(placeholder_value.as_bytes());
};
replace_placeholder(start_html_placeholder_pos, &start_html_pos_value);
replace_placeholder(end_html_placeholder_pos, &end_html_pos_value);
replace_placeholder(start_fragment_placeholder_pos, &start_fragment_pos_value);
replace_placeholder(end_fragment_placeholder_pos, &end_fragment_pos_value);
buffer
}
+4
View File
@@ -0,0 +1,4 @@
#![doc = include_str!("../README.md")]
pub mod bitmap;
pub mod html;
+12
View File
@@ -70,6 +70,18 @@ pub trait CliprdrBackend: AsAny + std::fmt::Debug + Send {
/// client's clipboard prior to `CLIPRDR` SVC initialization.
fn on_request_format_list(&mut self);
/// Called by [crate::Cliprdr] when copy sequence is finished.
/// This method is called after remote returns format list response.
///
/// Usefull for the backend implementations which need to know when remote is ready to paste
/// previously advertised formats from the client. E.g. Web client uses this for
/// Firefox-specific logic to delay sending keyboard key events to prevent pasting the old
/// data from the clipboard.
///
/// This method has default implementation which does nothing because it is not required for
/// most of the backends.
fn on_format_list_received(&mut self) {}
/// Adjusts [crate::Cliprdr] backend capabilities based on capabilities negotiated with a server.
///
/// Called by [crate::Cliprdr] when capability negotiation is finished and server capabilities are
+2
View File
@@ -131,6 +131,8 @@ impl Cliprdr {
} else {
info!("CLIPRDR(clipboard) Remote has received format list successfully");
}
self.backend.on_format_list_received();
}
FormatListResponse::Fail => {
return self.handle_error_transition(ClipboardError::FormatListRejected);
@@ -14,7 +14,7 @@ use crate::pdu::{ClipboardPduFlags, PartialHeader};
/// [Standard clipboard formats](https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats)
/// defined by Microsoft are available as constants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClipboardFormatId(u32);
pub struct ClipboardFormatId(pub u32);
impl ClipboardFormatId {
/// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination.
@@ -150,6 +150,9 @@ impl ClipboardFormatName {
/// data with [`crate::pdu::format_data::PackedFileList`] payload.
pub const FILE_LIST: Self = Self::new_static("FileGroupDescriptorW");
/// Special format defined by Windows to store HTML fragment in clipboard.
pub const HTML: Self = Self::new_static("HTML Format");
pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
Self(name.into())
}
@@ -167,8 +170,8 @@ impl ClipboardFormatName {
/// Represents `CLIPRDR_SHORT_FORMAT_NAME` and `CLIPRDR_LONG_FORMAT_NAME`
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClipboardFormat {
id: ClipboardFormatId,
name: Option<ClipboardFormatName>,
pub id: ClipboardFormatId,
pub name: Option<ClipboardFormatName>,
}
impl ClipboardFormat {
+2 -1
View File
@@ -14,4 +14,5 @@ arbitrary = { version = "1", features = ["derive"] }
ironrdp-graphics.workspace = true
ironrdp-pdu.workspace = true
ironrdp-cliprdr.workspace = true
ironrdp-rdpdr.workspace = true
ironrdp-rdpdr.workspace = true
ironrdp-cliprdr-format.workspace = true
+14
View File
@@ -118,3 +118,17 @@ pub fn rdp6_decode_bitmap_stream_to_rgb24(input: &BitmapInput<'_>) {
input.height as usize,
);
}
pub fn cliprdr_format(input: &[u8]) {
use ironrdp_cliprdr_format::bitmap::{dib_to_png, dibv5_to_png, png_to_cf_dib, png_to_cf_dibv5};
use ironrdp_cliprdr_format::html::{cf_html_to_text, text_to_cf_html};
let _ = png_to_cf_dib(input);
let _ = png_to_cf_dibv5(input);
let _ = dib_to_png(input);
let _ = dibv5_to_png(input);
let _ = cf_html_to_text(input);
let _ = text_to_cf_html(String::from_utf8_lossy(input).as_ref());
}
+6
View File
@@ -408,6 +408,12 @@ impl<'a> WriteCursor<'a> {
self.write_array(value.to_be_bytes())
}
#[inline]
#[track_caller]
pub fn write_i32(&mut self, value: i32) {
self.write_array(value.to_le_bytes())
}
#[inline]
#[track_caller]
pub fn write_u64(&mut self, value: u64) {
+1
View File
@@ -7,6 +7,7 @@
use core::fmt;
use cursor::WriteCursor;
#[cfg(feature = "alloc")]
use write_buf::WriteBuf;
use crate::cursor::ReadCursor;
+1 -1
View File
@@ -8,7 +8,7 @@ use ironrdp_svc::AsAny;
use crate::pdu::efs::{DeviceControlRequest, ServerDeviceAnnounceResponse, ServerDriveIoRequest};
use crate::pdu::esc::{ScardCall, ScardIoCtlCode};
/// OS-specific device redirection backend inteface.
/// OS-specific device redirection backend interface.
pub trait RdpdrBackend: AsAny + fmt::Debug + Send {
fn handle_server_device_announce_response(&mut self, pdu: ServerDeviceAnnounceResponse) -> PduResult<()>;
fn handle_scard_call(&mut self, req: DeviceControlRequest<ScardIoCtlCode>, call: ScardCall) -> PduResult<()>;
+1 -2
View File
@@ -23,10 +23,9 @@ use pdu::RdpdrPdu;
pub mod backend;
pub mod pdu;
use crate::pdu::efs::ServerDriveIoRequest;
pub use self::backend::noop::NoopRdpdrBackend;
pub use self::backend::RdpdrBackend;
use crate::pdu::efs::ServerDriveIoRequest;
/// The RDPDR channel as specified in [\[MS-RDPEFS\]].
///
+1
View File
@@ -25,6 +25,7 @@ paste = "1"
png = "0.17"
hex = "0.4"
ironrdp-cliprdr.workspace = true
ironrdp-cliprdr-format.workspace = true
ironrdp-connector.workspace = true
ironrdp-fuzzing.workspace = true
ironrdp-graphics.workspace = true

Some files were not shown because too many files have changed in this diff Show More