mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat: clipboard support for web client (#259)
This commit is contained in:
Generated
+12
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod bitmap;
|
||||
pub mod html;
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
use core::fmt;
|
||||
|
||||
use cursor::WriteCursor;
|
||||
#[cfg(feature = "alloc")]
|
||||
use write_buf::WriteBuf;
|
||||
|
||||
use crate::cursor::ReadCursor;
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
@@ -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\]].
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user