diff --git a/Cargo.lock b/Cargo.lock index 3a44371d84fd0..ee46eb114479a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arrayvec" @@ -25,9 +25,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "atomic-polyfill" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d862f14e042f75b95236d4ef1bb3d5c170964082d1e1e9c3ce689a2cbee217c" +checksum = "e14bf7b4f565e5e717d7a7a65b2a05c0b8c96e4db636d6f780f03b15108cdd1b" dependencies = [ "critical-section", ] @@ -55,7 +55,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" dependencies = [ - "rustc_version", + "rustc_version 0.2.3", ] [[package]] @@ -72,9 +72,9 @@ checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" [[package]] name = "base64ct" -version = "1.1.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" +checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" [[package]] name = "bit_field" @@ -123,9 +123,9 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byte-tools" @@ -165,9 +165,9 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "cortex-m" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ff967e867ca14eba0c34ac25cd71ea98c678e741e3915d923999bb2fe7c826" +checksum = "cd20d4ac4aa86f4f75f239d59e542ef67de87cce2c282818dc6e84155d3ea126" dependencies = [ "bare-metal 0.2.5", "bitfield", @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "critical-section" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e191a5a6f6edad9b679777ef6b6c0f2bdd4a333f2ecb8f61c3e28109a03d70" +checksum = "95da181745b56d4bd339530ec393508910c909c784e8962d15d722bacf0bcbcd" dependencies = [ "bare-metal 1.0.0", "cfg-if", @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if", "libc", @@ -404,13 +404,14 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "heapless" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d076121838e03f862871315477528debffdb7462fb229216ecef91b1a3eb31eb" +checksum = "8a08e755adbc0ad283725b29f4a4883deee15336f372d5f61fae59efec40f983" dependencies = [ "atomic-polyfill", "hash32", - "spin 0.9.2", + "rustc_version 0.4.0", + "spin 0.9.3", "stable_deref_trait", ] @@ -450,9 +451,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indexmap" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg", "hashbrown", @@ -528,9 +529,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libm" @@ -540,18 +541,19 @@ checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] @@ -581,9 +583,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "multimap" @@ -658,9 +660,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -668,9 +670,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" dependencies = [ "autocfg", "num-integer", @@ -679,9 +681,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", "libm", @@ -711,9 +713,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "opaque-debug" @@ -742,9 +744,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset 0.4.1", "indexmap", @@ -813,11 +815,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -842,7 +844,7 @@ dependencies = [ "lazy_static", "log", "multimap", - "petgraph 0.6.0", + "petgraph 0.6.2", "prost", "prost-types", "regex", @@ -875,9 +877,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -941,7 +943,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.5", + "getrandom 0.2.6", ] [[package]] @@ -994,18 +996,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -1014,9 +1016,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -1100,7 +1102,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.10", ] [[package]] @@ -1114,9 +1125,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" dependencies = [ "log", "ring", @@ -1126,9 +1137,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "scopeguard" @@ -1155,6 +1166,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" + [[package]] name = "semver-parser" version = "0.7.0" @@ -1163,9 +1180,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.136" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" [[package]] name = "smallvec" @@ -1181,9 +1198,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" dependencies = [ "lock_api", ] @@ -1224,13 +1241,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.89" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1268,9 +1285,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -1282,16 +1299,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] -name = "unicode-segmentation" -version = "1.9.0" +name = "unicode-ident" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-segmentation" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "untrusted" @@ -1311,7 +1328,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.5", + "getrandom 0.2.6", ] [[package]] diff --git a/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs b/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs index 3d38a4ddeef77..4c053bc495e53 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/cliprdr.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::util; use crate::errors::invalid_data_error; use crate::vchan::ChannelPDUFlags; use crate::{vchan, Payload}; @@ -22,6 +23,7 @@ use rdp::core::{mcs, tpkt}; use rdp::model::error::*; use rdp::try_let; use std::collections::HashMap; +use std::collections::VecDeque; use std::io::{Cursor, Read, Write}; pub const CHANNEL_NAME: &str = "cliprdr"; @@ -47,6 +49,7 @@ pub struct Client { clipboard: HashMap>, pending: PendingData, on_remote_copy: Box)>, + incoming_paste_formats: VecDeque, } impl Default for Client { @@ -65,6 +68,7 @@ impl Client { clipboard_header: None, }, on_remote_copy, + incoming_paste_formats: VecDeque::new(), } } @@ -151,38 +155,36 @@ impl Client { /// update_clipboard is invoked from Go. /// It updates the local clipboard cache and returns the encoded message /// that should be sent to the RDP server. - pub fn update_clipboard(&mut self, data: Vec) -> RdpResult>> { - const CR: u8 = 13; - const LF: u8 = 10; - - // convert LF to CRLF, as required by CF_OEMTEXT - let len_orig = data.len(); - let mut converted = Vec::with_capacity(len_orig); - for i in 0..len_orig { - match data[i] { - LF => { + pub fn update_clipboard(&mut self, data: String) -> RdpResult>> { + // convert LF to CRLF, as required by CF_TEXT and CF_UNICODETEXT + let mut converted = String::with_capacity(data.len()); + let mut prev_was_cr = false; + for c in data.chars() { + match c { + '\n' if !prev_was_cr => { // convert LF to CRLF, so long as the previous character // wasn't CR (in which case there's no conversion necessary) - if i == 0 || (data[i - 1] != CR) { - converted.push(CR); + converted.push('\r'); + converted.push('\n'); + } + c => { + converted.push(c); + if c == '\r' { + prev_was_cr = true; + continue; } - converted.push(LF); } - _ => converted.push(data[i]), } - } - // Windows requires a null terminator, so add one if necessary - if !converted.is_empty() && converted[converted.len() - 1] != 0x00 { - converted.push(0x00); - } - self.clipboard - .insert(ClipboardFormat::CF_OEMTEXT as u32, converted); + prev_was_cr = false; + } + let (data, format) = encode_clipboard(converted); + self.clipboard.insert(format as u32, data); encode_message( ClipboardPDUType::CB_FORMAT_LIST, FormatListPDU { - format_names: vec![LongFormatName::id(ClipboardFormat::CF_OEMTEXT as u32)], + format_names: vec![LongFormatName::id(format as u32)], } .encode()?, ) @@ -236,42 +238,47 @@ impl Client { /// Handles the format list PDU, which is a notification from the server /// that some data was copied and can be requested at a later date. - fn handle_format_list(&self, payload: &mut Payload, length: u32) -> RdpResult>> { + fn handle_format_list( + &mut self, + payload: &mut Payload, + length: u32, + ) -> RdpResult>> { let list = FormatListPDU::::decode(payload, length)?; - debug!( - "{:?} data was copied on the RDP server", - list.format_names - .iter() - .map(|n| n.format_id) - .collect::>() - ); - - // if we want to support a variety of formats, we should clear - // and re-initialize some local state (Clipboard Format ID Map) - // - // we're only supporting standard (text) formats right now, so - // we don't need to maintain a local/remote mapping - // - // see section 3.1.1.1 for details + let formats = list + .format_names + .iter() + .map(|n| n.format_id) + .collect::>(); + debug!("{:?} data was copied on the RDP server", formats); let mut result = encode_message(ClipboardPDUType::CB_FORMAT_LIST_RESPONSE, vec![])?; - for name in list.format_names { - match FromPrimitive::from_u32(name.format_id) { - // TODO(zmb3): support CF_TEXT, CF_UNICODETEXT, ... - Some(ClipboardFormat::CF_OEMTEXT) => { - // request the data by imitating a paste event - result.extend(encode_message( - ClipboardPDUType::CB_FORMAT_DATA_REQUEST, - FormatDataRequestPDU::for_id(name.format_id).encode()?, - )?); - } - _ => { - info!("{:?} data was copied on the remote desktop, but this format is unsupported", name.format_id); - } - } + let request_format; + if formats.contains(&(ClipboardFormat::CF_UNICODETEXT as u32)) { + request_format = ClipboardFormat::CF_UNICODETEXT; + } else if formats.contains(&(ClipboardFormat::CF_TEXT as u32)) { + request_format = ClipboardFormat::CF_TEXT; + } else if formats.contains(&(ClipboardFormat::CF_OEMTEXT as u32)) { + request_format = ClipboardFormat::CF_OEMTEXT; + } else { + info!( + "{:?} data was copied on the remote desktop, but no supported formats were found", + formats + ); + + return Ok(result); } + // Record the format of the data we're requesting so we can correcly decode the response later. + // Response events are globally ordered so we use a FIFO queue for format tracking. + self.incoming_paste_formats.push_back(request_format); + + // request the data by imitating a paste event. + result.extend(encode_message( + ClipboardPDUType::CB_FORMAT_DATA_REQUEST, + FormatDataRequestPDU::for_id(request_format as u32).encode()?, + )?); + Ok(result) } @@ -316,21 +323,21 @@ impl Client { payload: &mut Payload, length: u32, ) -> RdpResult>> { - let mut resp = FormatDataResponsePDU::decode(payload, length)?; + let resp = FormatDataResponsePDU::decode(payload, length)?; let data_len = resp.data.len(); + let format = self.incoming_paste_formats.pop_front().ok_or_else(|| { + invalid_data_error( + "no expected format found, possibly received too many format data responses", + ) + })?; + debug!( - "recieved {} bytes of copied data from Windows Desktop", - data_len, + "received {} bytes of copied data from Windows Desktop with format {:?}", + data_len, format, ); - // trim the null-terminator, if it exists - // (but don't worry about CRLF conversion, most non-Windows systems can handle CRLF well enough) - if let Some(0x00) = resp.data.last() { - resp.data.truncate(resp.data.len() - 1); - } - - (self.on_remote_copy)(resp.data); - + let decoded = decode_clipboard(resp.data, format)?; + (self.on_remote_copy)(decoded); Ok(vec![]) } } @@ -548,6 +555,53 @@ impl FormatListPDU { } } +// encode_clipboard encodes data suitably for clipboard storage. +// This means determining the appropriate format and making sure the data has a nul terminator. +// +// This data must be valid UTF-8. +fn encode_clipboard(mut data: String) -> (Vec, ClipboardFormat) { + if data.is_ascii() { + if matches!(data.chars().last(), Some(x) if x != '\0') { + data.push('\0'); + } + + (data.into_bytes(), ClipboardFormat::CF_TEXT) + } else { + let encoded = util::to_nul_terminated_utf16le(&data); + (encoded, ClipboardFormat::CF_UNICODETEXT) + } +} + +// decode_clipboard decodes data from a given clipboard format into UTF-8. +fn decode_clipboard(mut data: Vec, format: ClipboardFormat) -> RdpResult> { + match format { + ClipboardFormat::CF_TEXT | ClipboardFormat::CF_OEMTEXT => { + if data.last().copied() == Some(b'\0') { + data.pop(); + } + + Ok(data) + } + ClipboardFormat::CF_UNICODETEXT => { + let mut data = data.as_slice(); + let clip = data.len() - 2; + if data.len() >= 2 && data[clip..] == [0, 0] { + data = &data[..clip]; + } + + let units: Vec = data + .chunks_exact(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .collect(); + + Ok(String::from_utf16_lossy(&units).into_bytes()) + } + _ => Err(invalid_data_error( + "attempted to decode unsupported clipboard format", + )), + } +} + /// Represents the CLIPRDR_SHORT_FORMAT_NAME structure. #[derive(Debug)] struct ShortFormatName { @@ -625,10 +679,7 @@ impl FormatName for LongFormatName { // must be encoded as a single Unicode null character (two zero bytes) None => w.write_u16::(0)?, Some(name) => { - for c in str::encode_utf16(name) { - w.write_u16::(c)?; - } - w.write_u16::(0)?; // terminating null + w.append(&mut util::to_nul_terminated_utf16le(name)); } }; @@ -1044,16 +1095,13 @@ mod tests { #[test] fn responds_to_format_data_request_hasdata() { // a null-terminated utf-16 string, represented as a Vec - let test_data: Vec = "test\0" - .encode_utf16() - .flat_map(|v| v.to_le_bytes()) - .collect(); + let test_data = util::to_nul_terminated_utf16le("test"); let mut c: Client = Default::default(); c.clipboard - .insert(ClipboardFormat::CF_OEMTEXT as u32, test_data.clone()); + .insert(ClipboardFormat::CF_UNICODETEXT as u32, test_data.clone()); - let req = FormatDataRequestPDU::for_id(ClipboardFormat::CF_OEMTEXT as u32); + let req = FormatDataRequestPDU::for_id(ClipboardFormat::CF_UNICODETEXT as u32); let responses = c .handle_format_data_request(&mut Cursor::new(req.encode().unwrap())) .unwrap(); @@ -1078,15 +1126,27 @@ mod tests { send.send(vec).unwrap(); })); + let data_format_list = FormatListPDU { + format_names: vec![LongFormatName { + format_id: ClipboardFormat::CF_TEXT as u32, + format_name: None, + }], + } + .encode() + .unwrap(); + let data_resp = FormatDataResponsePDU { data: String::from("abc\0").into_bytes(), } .encode() .unwrap(); - let len = data_resp.len(); + let mut len = data_format_list.len() as u32; + c.handle_format_list(&mut Cursor::new(data_format_list), len) + .unwrap(); - c.handle_format_data_response(&mut Cursor::new(data_resp), len as u32) + len = data_resp.len() as u32; + c.handle_format_data_response(&mut Cursor::new(data_resp), len) .unwrap(); // ensure that the null terminator was trimmed @@ -1097,9 +1157,7 @@ mod tests { #[test] fn update_clipboard_returns_format_list_pdu() { let mut c: Client = Default::default(); - let messages = c - .update_clipboard(String::from("abc").into_bytes()) - .unwrap(); + let messages = c.update_clipboard("abc".to_owned()).unwrap(); let bytes = messages[0].clone(); // verify that it returns a properly encoded format list PDU @@ -1111,7 +1169,7 @@ mod tests { assert_eq!(ClipboardPDUType::CB_FORMAT_LIST, header.msg_type); assert_eq!(1, format_list.format_names.len()); assert_eq!( - ClipboardFormat::CF_OEMTEXT as u32, + ClipboardFormat::CF_TEXT as u32, format_list.format_names[0].format_id ); @@ -1119,30 +1177,37 @@ mod tests { // (with a null-terminating character) assert_eq!( String::from("abc\0").into_bytes(), - *c.clipboard - .get(&(ClipboardFormat::CF_OEMTEXT as u32)) - .unwrap() + *c.clipboard.get(&(ClipboardFormat::CF_TEXT as u32)).unwrap() ); } #[test] fn update_clipboard_conversion() { - for (input, expected) in &[ - ("abc\0", "abc\0"), // already null-terminated, no conversion necessary - ("\n123", "\r\n123\0"), // starts with LF - ("def\r\n", "def\r\n\0"), // already CRLF, no conversion necessary - ("gh\r\nij\nk", "gh\r\nij\r\nk\0"), // mixture of both + struct Item(&'static str, &'static [u8], ClipboardFormat); + for Item(input, expected, format) in [ + Item("abc\0", b"abc\0", ClipboardFormat::CF_TEXT), // already null-terminated, no conversion necessary + Item("\n123", b"\r\n123\0", ClipboardFormat::CF_TEXT), // starts with LF + Item("def\r\n", b"def\r\n\0", ClipboardFormat::CF_TEXT), // already CRLF, no conversion necessary + Item("gh\r\nij\nk", b"gh\r\nij\r\nk\0", ClipboardFormat::CF_TEXT), // mixture of both + Item( + "🤑\n", + &[62, 216, 17, 221, b'\r', 0, b'\n', 0, 0, 0], + ClipboardFormat::CF_UNICODETEXT, + ), // detection and utf8 -> utf16 conversion & CRLF conversion + Item( + "🤑\r\n", + &[62, 216, 17, 221, b'\r', 0, b'\n', 0, 0, 0], + ClipboardFormat::CF_UNICODETEXT, + ), // detection and utf8 -> utf16 conversion & no CRLF conversion ] { let mut c: Client = Default::default(); - c.update_clipboard(String::from(*input).into_bytes()) - .unwrap(); + c.update_clipboard(input.to_owned()).unwrap(); + assert_eq!( - String::from(*expected).into_bytes(), - *c.clipboard - .get(&(ClipboardFormat::CF_OEMTEXT as u32)) - .unwrap(), + expected, + *c.clipboard.get(&(format as u32)).unwrap(), "testing {}", - input + input, ); } } diff --git a/lib/srv/desktop/rdp/rdpclient/src/lib.rs b/lib/srv/desktop/rdp/rdpclient/src/lib.rs index 0df82109d0615..41c7f8f3ebae3 100644 --- a/lib/srv/desktop/rdp/rdpclient/src/lib.rs +++ b/lib/srv/desktop/rdp/rdpclient/src/lib.rs @@ -17,6 +17,7 @@ pub mod errors; pub mod piv; pub mod rdpdr; pub mod scard; +pub mod util; pub mod vchan; #[macro_use] @@ -408,7 +409,9 @@ pub unsafe extern "C" fn update_clipboard( let mut lock = client.rdp_client.lock().unwrap(); match lock.cliprdr { - Some(ref mut clip) => match clip.update_clipboard(data) { + Some(ref mut clip) => match clip + .update_clipboard(String::from_utf8_lossy(&data).into_owned()) + { Ok(messages) => { for message in messages { if let Err(e) = lock.mcs.write(&cliprdr::CHANNEL_NAME.to_string(), message) { diff --git a/lib/srv/desktop/rdp/rdpclient/src/util.rs b/lib/srv/desktop/rdp/rdpclient/src/util.rs new file mode 100644 index 0000000000000..087220ea1cd24 --- /dev/null +++ b/lib/srv/desktop/rdp/rdpclient/src/util.rs @@ -0,0 +1,28 @@ +// Copyright 2022 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// According to [MS-RDPEFS] 1.1 Glossary: +/// Unless otherwise specified, all Unicode strings follow the UTF-16LE +/// encoding scheme with no Byte Order Mark (BOM). +/// +/// This helper function takes a string slice and converts it to a +/// UTF-16LE encoded Vec, which is useful in cases where we want +/// to handle some data in the code as a &str (or String), and later +/// convert it to RDP's preferred format and send it over the wire. +pub fn to_nul_terminated_utf16le(s: &str) -> Vec { + s.encode_utf16() + .chain([0]) + .flat_map(|v| v.to_le_bytes()) + .collect() +}