clients::rdp connects over any caller-dialled TCP stream (direct, proxy or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM), optionally falling back to TLS-only logon for servers without NLA such as xrdp (credentials are passed so they log straight in). The server certificate is pinned through a caller check on its SHA256 fingerprint before any credentials are sent. The desktop decodes into the VNC client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse operations), Display Control resizes and the deactivation-reactivation sequence run on the session thread. IronRDP's releases need three vendored patches (vendor/README.md): picky and sspi with their RustCrypto release-candidate pins relaxed so they share russh's stable versions (their own suites, 402 tests, pass against them), and ironrdp-session with two unreleased upstream bitmap fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops render sheared. Verified live against xrdp (scottyhardy/docker-remote-desktop): logon, full desktop rendering, keyboard and mouse input.
822 lines
35 KiB
Rust
822 lines
35 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ironrdp_bulk::BulkCompressor;
|
|
use ironrdp_core::{DecodeErrorKind, ReadCursor, WriteBuf, decode_cursor};
|
|
use ironrdp_graphics::image_processing::PixelFormat;
|
|
use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget};
|
|
use ironrdp_graphics::rdp6::BitmapStreamDecoder;
|
|
use ironrdp_graphics::rle::RlePixelFormat;
|
|
use ironrdp_pdu::bitmap::BitmapUpdateData;
|
|
use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu;
|
|
use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation};
|
|
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
|
|
use ironrdp_pdu::pointer::PointerUpdateData;
|
|
use ironrdp_pdu::rdp::capability_sets::{CODEC_ID_NONE, CODEC_ID_REMOTEFX, CodecId};
|
|
use ironrdp_pdu::rdp::headers::{CompressionFlags, ShareDataPdu};
|
|
use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand};
|
|
use tracing::{debug, trace, warn};
|
|
|
|
use crate::image::DecodedImage;
|
|
use crate::palette::Palette;
|
|
use crate::pointer::PointerCache;
|
|
use crate::{SessionError, SessionErrorExt as _, SessionResult, custom_err, reason_err, rfx};
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ironrdp_pdu::bitmap::{BitmapData, Compression};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn raw_bitmap_removes_byte_padding_without_changing_source_stride() {
|
|
let mut processor = ProcessorBuilder {
|
|
io_channel_id: 0,
|
|
user_channel_id: 0,
|
|
share_id: 0,
|
|
enable_server_pointer: false,
|
|
pointer_software_rendering: false,
|
|
bulk_decompressor: None,
|
|
}
|
|
.build();
|
|
let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3);
|
|
|
|
// Two bottom-up BGR rows of three source pixels each. The final source
|
|
// column is outside the destination and each row has three pad bytes.
|
|
let bitmap_data = [
|
|
3, 2, 1, 6, 5, 4, 9, 8, 7, 0xff, 0xff, 0xff, // bottom row
|
|
15, 14, 13, 18, 17, 16, 21, 20, 19, 0xff, 0xff, 0xff, // top row
|
|
];
|
|
let bitmap_update = BitmapUpdateData {
|
|
rectangles: vec![BitmapData {
|
|
rectangle: InclusiveRectangle {
|
|
left: 1,
|
|
top: 0,
|
|
right: 2,
|
|
bottom: 1,
|
|
},
|
|
width: 3,
|
|
height: 2,
|
|
bits_per_pixel: 24,
|
|
compression_flags: Compression::empty(),
|
|
compressed_data_header: None,
|
|
bitmap_data: &bitmap_data,
|
|
}],
|
|
};
|
|
|
|
processor.process_bitmap_update(&mut image, bitmap_update).unwrap();
|
|
|
|
let pixel = |x: usize, y: usize| -> [u8; 4] {
|
|
let offset = (y * usize::from(image.width()) + x) * 4;
|
|
image.data()[offset..offset + 4]
|
|
.try_into()
|
|
.expect("pixel has four channels")
|
|
};
|
|
assert_eq!(pixel(1, 0), [13, 14, 15, 255]);
|
|
assert_eq!(pixel(2, 0), [16, 17, 18, 255]);
|
|
assert_eq!(pixel(1, 1), [1, 2, 3, 255]);
|
|
assert_eq!(pixel(2, 1), [4, 5, 6, 255]);
|
|
assert_eq!(pixel(3, 0), [0, 0, 0, 0]);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum UpdateKind {
|
|
None,
|
|
Region(InclusiveRectangle),
|
|
PointerDefault,
|
|
PointerHidden,
|
|
PointerPosition { x: u16, y: u16 },
|
|
PointerBitmap(Arc<DecodedPointer>),
|
|
}
|
|
|
|
pub struct Processor {
|
|
complete_data: CompleteData,
|
|
rfx_handler: rfx::DecodingContext,
|
|
marker_processor: FrameMarkerProcessor,
|
|
bitmap_stream_decoder: BitmapStreamDecoder,
|
|
pointer_cache: PointerCache,
|
|
use_system_pointer: bool,
|
|
mouse_pos_update: Option<(u16, u16)>,
|
|
enable_server_pointer: bool,
|
|
pointer_software_rendering: bool,
|
|
/// Bulk decompressor for server-to-client compressed PDUs.
|
|
/// `None` when compression was not negotiated.
|
|
bulk_decompressor: Option<BulkCompressor>,
|
|
/// Current 8bpp color palette. Updated by Palette fast-path updates.
|
|
palette: Palette,
|
|
#[cfg(feature = "qoiz")]
|
|
zdctx: zstd_safe::DCtx<'static>,
|
|
}
|
|
|
|
impl Processor {
|
|
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
|
|
self.mouse_pos_update = Some((x, y));
|
|
}
|
|
|
|
/// Process input fast path frame and return list of updates.
|
|
pub fn process(
|
|
&mut self,
|
|
image: &mut DecodedImage,
|
|
input: &[u8],
|
|
output: &mut WriteBuf,
|
|
) -> SessionResult<Vec<UpdateKind>> {
|
|
let mut processor_updates = Vec::new();
|
|
|
|
if let Some((x, y)) = self.mouse_pos_update.take() {
|
|
if let Some(rect) = image.move_pointer(x, y)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
|
|
let mut input = ReadCursor::new(input);
|
|
|
|
let header = decode_cursor::<FastPathHeader>(&mut input).map_err(SessionError::decode)?;
|
|
trace!(fast_path_header = ?header, "Received Fast-Path packet");
|
|
|
|
// A single FastPath output PDU can contain multiple updates.
|
|
// Loop over all updates within the PDU payload.
|
|
while !input.is_empty() {
|
|
let update_result = self.process_single_update(&mut input, image, output)?;
|
|
processor_updates.extend(update_result);
|
|
}
|
|
|
|
Ok(processor_updates)
|
|
}
|
|
|
|
/// Process a single FastPath update from the cursor, advancing past it.
|
|
fn process_single_update(
|
|
&mut self,
|
|
input: &mut ReadCursor<'_>,
|
|
image: &mut DecodedImage,
|
|
output: &mut WriteBuf,
|
|
) -> SessionResult<Vec<UpdateKind>> {
|
|
let mut processor_updates = Vec::new();
|
|
|
|
let update_pdu = decode_cursor::<FastPathUpdatePdu<'_>>(input).map_err(SessionError::decode)?;
|
|
trace!(fast_path_update_fragmentation = ?update_pdu.fragmentation);
|
|
|
|
// Decompress the payload if the server sent it compressed.
|
|
let decompressed_data;
|
|
let payload = if let Some(flags) = update_pdu.compression_flags {
|
|
if flags.contains(CompressionFlags::COMPRESSED) || flags.contains(CompressionFlags::FLUSHED) {
|
|
let bulk_flags =
|
|
u32::from(flags.bits()) | u32::from(update_pdu.compression_type.map_or(0, |ct| ct.as_u8()));
|
|
|
|
if let Some(ref mut decompressor) = self.bulk_decompressor {
|
|
let decompressed = decompressor
|
|
.decompress(update_pdu.data, bulk_flags)
|
|
.map_err(|e| reason_err!("FastPath", "bulk decompression failed: {}", e))?;
|
|
// Copy decompressed data before accessing metrics (releases the mutable borrow).
|
|
decompressed_data = decompressed.to_vec();
|
|
debug!(
|
|
compressed_size = update_pdu.data.len(),
|
|
decompressed_size = decompressed_data.len(),
|
|
compression_type = ?update_pdu.compression_type,
|
|
compression_ratio = format_args!("{:.2}x", decompressor.compression_ratio()),
|
|
total_compressed = decompressor.total_compressed_bytes(),
|
|
total_uncompressed = decompressor.total_uncompressed_bytes(),
|
|
"Decompressed FastPath update"
|
|
);
|
|
decompressed_data.as_slice()
|
|
} else {
|
|
warn!("Received compressed FastPath data but no decompressor is configured");
|
|
update_pdu.data
|
|
}
|
|
} else {
|
|
// Compression flags present but COMPRESSED bit not set — pass data through.
|
|
// Still need to inform the decompressor of FLUSHED/AT_FRONT flags even
|
|
// without compressed payload.
|
|
update_pdu.data
|
|
}
|
|
} else {
|
|
update_pdu.data
|
|
};
|
|
|
|
let processed_complete_data = self.complete_data.process_data(payload, update_pdu.fragmentation);
|
|
|
|
let update_code = update_pdu.update_code;
|
|
|
|
let Some(data) = processed_complete_data else {
|
|
return Ok(processor_updates);
|
|
};
|
|
|
|
let update = FastPathUpdate::decode_with_code(data.as_slice(), update_code);
|
|
|
|
match update {
|
|
Ok(FastPathUpdate::SurfaceCommands(surface_commands)) => {
|
|
trace!("Received Surface Commands: {} pieces", surface_commands.len());
|
|
let update_region = self.process_surface_commands(image, output, surface_commands)?;
|
|
processor_updates.push(UpdateKind::Region(update_region));
|
|
}
|
|
Ok(FastPathUpdate::Bitmap(bitmap_update)) => {
|
|
trace!("Received bitmap update");
|
|
let updates = self.process_bitmap_update(image, bitmap_update)?;
|
|
processor_updates.extend(updates);
|
|
}
|
|
Ok(FastPathUpdate::Pointer(update)) => {
|
|
let updates = self.process_pointer_update(image, update)?;
|
|
processor_updates.extend(updates);
|
|
}
|
|
Ok(FastPathUpdate::Palette(palette_data)) => {
|
|
trace!("Received palette update");
|
|
self.palette.process_update(palette_data);
|
|
}
|
|
Err(e) => {
|
|
// FIXME: This seems to be a way of special-handling the error case in FastPathUpdate::decode_cursor_with_code
|
|
// to ignore the unsupported update PDUs, but this is a fragile logic and the rationale behind it is not
|
|
// obvious.
|
|
if let DecodeErrorKind::InvalidField { field, reason } = e.kind() {
|
|
warn!(field, reason, "Received invalid Fast-Path update");
|
|
processor_updates.push(UpdateKind::None);
|
|
} else {
|
|
return Err(custom_err!("Fast-Path", e));
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(processor_updates)
|
|
}
|
|
|
|
/// Process a bitmap update, shared between fast-path and slow-path pipelines.
|
|
pub fn process_bitmap_update(
|
|
&mut self,
|
|
image: &mut DecodedImage,
|
|
bitmap_update: BitmapUpdateData<'_>,
|
|
) -> SessionResult<Vec<UpdateKind>> {
|
|
let mut buf = Vec::new();
|
|
let mut update_kind = UpdateKind::None;
|
|
|
|
for update in bitmap_update.rectangles {
|
|
trace!("{update:?}");
|
|
buf.clear();
|
|
|
|
let width = update.width.min(update.rectangle.width());
|
|
let height = update.height.min(update.rectangle.height());
|
|
if width == 0 || height == 0 {
|
|
warn!(
|
|
bitmap_width = update.width,
|
|
bitmap_height = update.height,
|
|
destination = ?update.rectangle,
|
|
"Skipping bitmap with an empty source or destination"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// `width` and `height` describe the source bitmap layout, while
|
|
// destination bounds describe where its visible portion is drawn.
|
|
// Servers can send source rows wider than the destination; preserve
|
|
// that source stride and discard only the excess source extent.
|
|
let bitmap_rectangle = InclusiveRectangle {
|
|
left: update.rectangle.left,
|
|
top: update.rectangle.top,
|
|
right: update.rectangle.left + width - 1,
|
|
bottom: update.rectangle.top + height - 1,
|
|
};
|
|
|
|
// Bitmap data is either compressed or uncompressed, depending
|
|
// on whether the BITMAP_COMPRESSION flag is present in the
|
|
// flags field.
|
|
let update_rectangle = if update
|
|
.compression_flags
|
|
.contains(ironrdp_pdu::bitmap::Compression::BITMAP_COMPRESSION)
|
|
{
|
|
if update.bits_per_pixel == 32 {
|
|
// Compressed bitmaps at a color depth of 32 bpp are compressed using RDP 6.0
|
|
// Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream
|
|
// structure ([MS-RDPEGDI] section 2.2.2.5.1).
|
|
debug!("32 bpp compressed RDP6_BITMAP_STREAM");
|
|
|
|
match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24(
|
|
update.bitmap_data,
|
|
&mut buf,
|
|
usize::from(update.width),
|
|
usize::from(update.height),
|
|
) {
|
|
// RDP6 bitmap streams are bottom-up (upstream #1524).
|
|
Ok(()) => image.apply_rgb24(&buf, &bitmap_rectangle, update.width, true)?,
|
|
Err(err) => {
|
|
warn!("Invalid RDP6_BITMAP_STREAM: {err}");
|
|
update.rectangle.clone()
|
|
}
|
|
}
|
|
} else {
|
|
// Compressed bitmaps not in 32 bpp format are compressed using Interleaved
|
|
// RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section
|
|
// 2.2.9.1.1.3.1.2.4).
|
|
debug!(bpp = update.bits_per_pixel, "Non-32 bpp compressed RLE_BITMAP_STREAM",);
|
|
|
|
match ironrdp_graphics::rle::decompress(
|
|
update.bitmap_data,
|
|
&mut buf,
|
|
usize::from(update.width),
|
|
usize::from(update.height),
|
|
usize::from(update.bits_per_pixel),
|
|
) {
|
|
Ok(RlePixelFormat::Rgb16) => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
Ok(RlePixelFormat::Rgb15) => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
Ok(RlePixelFormat::Rgb24) => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
Ok(RlePixelFormat::Rgb8) => image.apply_rgb8_with_palette(
|
|
&buf,
|
|
&bitmap_rectangle,
|
|
self.palette.colors(),
|
|
update.width,
|
|
)?,
|
|
|
|
Err(e) => {
|
|
warn!("Invalid RLE-compressed bitmap: {e}");
|
|
update.rectangle.clone()
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Uncompressed bitmap data is formatted as a bottom-up, left-to-right series of
|
|
// pixels. Each pixel is a whole number of bytes. Each row contains a multiple of
|
|
// four bytes (including up to three bytes of padding, as necessary).
|
|
// [MS-RDPBCGR] 2.2.9.1.1.3.1.2.2
|
|
trace!("Uncompressed raw bitmap");
|
|
|
|
let bpp = usize::from(update.bits_per_pixel);
|
|
let width = usize::from(update.width);
|
|
let bytes_per_pixel = bpp.div_ceil(8);
|
|
let row_bytes = width * bytes_per_pixel;
|
|
let padded_row_bytes = (row_bytes + 3) & !3;
|
|
|
|
if padded_row_bytes != row_bytes {
|
|
// Strip only byte padding; the tightly packed rows still retain
|
|
// `update.width` pixels and therefore their source stride.
|
|
buf.clear();
|
|
for row in update
|
|
.bitmap_data
|
|
.chunks_exact(padded_row_bytes)
|
|
.take(usize::from(update.height))
|
|
{
|
|
buf.extend_from_slice(&row[..row_bytes]);
|
|
}
|
|
|
|
match update.bits_per_pixel {
|
|
8 => image.apply_rgb8_with_palette(
|
|
&buf,
|
|
&bitmap_rectangle,
|
|
self.palette.colors(),
|
|
update.width,
|
|
)?,
|
|
15 => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
16 => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
24 => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
|
32 => image.apply_rgb32_bitmap(&buf, PixelFormat::BgrX32, &bitmap_rectangle, update.width)?,
|
|
_ => {
|
|
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
|
update.rectangle.clone()
|
|
}
|
|
}
|
|
} else {
|
|
match update.bits_per_pixel {
|
|
8 => image.apply_rgb8_with_palette(
|
|
update.bitmap_data,
|
|
&bitmap_rectangle,
|
|
self.palette.colors(),
|
|
update.width,
|
|
)?,
|
|
15 => image.apply_rgb15_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
|
16 => image.apply_rgb16_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
|
24 => image.apply_bgr24_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
|
32 => image.apply_rgb32_bitmap(
|
|
update.bitmap_data,
|
|
PixelFormat::BgrX32,
|
|
&bitmap_rectangle,
|
|
update.width,
|
|
)?,
|
|
_ => {
|
|
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
|
update.rectangle.clone()
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
match update_kind {
|
|
UpdateKind::Region(current) => update_kind = UpdateKind::Region(current.union(&update_rectangle)),
|
|
_ => update_kind = UpdateKind::Region(update_rectangle),
|
|
}
|
|
}
|
|
|
|
Ok(vec![update_kind])
|
|
}
|
|
|
|
/// Process a pointer update, shared between fast-path and slow-path pipelines.
|
|
pub fn process_pointer_update(
|
|
&mut self,
|
|
image: &mut DecodedImage,
|
|
update: PointerUpdateData<'_>,
|
|
) -> SessionResult<Vec<UpdateKind>> {
|
|
let mut processor_updates = Vec::new();
|
|
|
|
if !self.enable_server_pointer {
|
|
return Ok(processor_updates);
|
|
}
|
|
|
|
let bitmap_target = if self.pointer_software_rendering {
|
|
PointerBitmapTarget::Software
|
|
} else {
|
|
PointerBitmapTarget::Accelerated
|
|
};
|
|
|
|
match update {
|
|
PointerUpdateData::SetHidden => {
|
|
processor_updates.push(UpdateKind::PointerHidden);
|
|
if self.pointer_software_rendering && !self.use_system_pointer {
|
|
self.use_system_pointer = true;
|
|
if let Some(rect) = image.hide_pointer()? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
}
|
|
PointerUpdateData::SetDefault => {
|
|
processor_updates.push(UpdateKind::PointerDefault);
|
|
if self.pointer_software_rendering && !self.use_system_pointer {
|
|
self.use_system_pointer = true;
|
|
if let Some(rect) = image.hide_pointer()? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
}
|
|
PointerUpdateData::SetPosition(position) => {
|
|
if self.use_system_pointer || !self.pointer_software_rendering {
|
|
processor_updates.push(UpdateKind::PointerPosition {
|
|
x: position.x,
|
|
y: position.y,
|
|
});
|
|
} else if let Some(rect) = image.move_pointer(position.x, position.y)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
PointerUpdateData::Color(pointer) => {
|
|
let cache_index = pointer.cache_index;
|
|
|
|
let decoded_pointer = Arc::new(
|
|
DecodedPointer::decode_color_pointer_attribute(&pointer, bitmap_target)
|
|
.map_err(|e| SessionError::custom("failed to decode color pointer attribute", e))?,
|
|
);
|
|
|
|
let _ = self
|
|
.pointer_cache
|
|
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
|
|
|
if !self.pointer_software_rendering {
|
|
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
|
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
PointerUpdateData::Cached(cached) => {
|
|
let cache_index = cached.cache_index;
|
|
|
|
if let Some(cached_pointer) = self.pointer_cache.get(usize::from(cache_index)) {
|
|
// Disable system pointer
|
|
processor_updates.push(UpdateKind::PointerHidden);
|
|
self.use_system_pointer = false;
|
|
// Send graphics update
|
|
if !self.pointer_software_rendering {
|
|
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&cached_pointer)));
|
|
} else if let Some(rect) = image.update_pointer(cached_pointer)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
} else {
|
|
// In case pointer was hidden previously
|
|
if let Some(rect) = image.show_pointer()? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
} else {
|
|
warn!("Cached pointer not found {}", cache_index);
|
|
}
|
|
}
|
|
PointerUpdateData::New(pointer) => {
|
|
let cache_index = pointer.color_pointer.cache_index;
|
|
|
|
let decoded_pointer = Arc::new(
|
|
DecodedPointer::decode_pointer_attribute(&pointer, bitmap_target)
|
|
.map_err(|e| SessionError::custom("failed to decode pointer attribute", e))?,
|
|
);
|
|
|
|
let _ = self
|
|
.pointer_cache
|
|
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
|
|
|
if !self.pointer_software_rendering {
|
|
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
|
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
PointerUpdateData::Large(pointer) => {
|
|
let cache_index = pointer.cache_index;
|
|
|
|
let decoded_pointer: Arc<DecodedPointer> = Arc::new(
|
|
DecodedPointer::decode_large_pointer_attribute(&pointer, bitmap_target)
|
|
.map_err(|e| SessionError::custom("failed to decode large pointer attribute", e))?,
|
|
);
|
|
|
|
let _ = self
|
|
.pointer_cache
|
|
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
|
|
|
if !self.pointer_software_rendering {
|
|
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
|
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
|
processor_updates.push(UpdateKind::Region(rect));
|
|
}
|
|
}
|
|
};
|
|
|
|
Ok(processor_updates)
|
|
}
|
|
|
|
fn process_surface_commands(
|
|
&mut self,
|
|
image: &mut DecodedImage,
|
|
output: &mut WriteBuf,
|
|
surface_commands: Vec<SurfaceCommand<'_>>,
|
|
) -> SessionResult<InclusiveRectangle> {
|
|
let mut update_rectangle = None;
|
|
|
|
for command in surface_commands {
|
|
match command {
|
|
SurfaceCommand::SetSurfaceBits(bits) | SurfaceCommand::StreamSurfaceBits(bits) => {
|
|
let codec_id = CodecId::from_u8(bits.extended_bitmap_data.codec_id).ok_or_else(|| {
|
|
reason_err!(
|
|
"Fast-Path",
|
|
"unexpected codec ID: {:x}",
|
|
bits.extended_bitmap_data.codec_id
|
|
)
|
|
})?;
|
|
|
|
trace!(?codec_id, "Surface bits");
|
|
|
|
let destination = bits.destination;
|
|
// TODO(@pacmancoder): Correct rectangle conversion logic should
|
|
// be revisited when `rectangle_processing.rs` from
|
|
// `ironrdp-graphics` will be refactored to use generic `Rectangle`
|
|
// trait instead of hardcoded `InclusiveRectangle`.
|
|
let destination = InclusiveRectangle {
|
|
left: destination.left,
|
|
top: destination.top,
|
|
right: destination.right - 1,
|
|
bottom: destination.bottom - 1,
|
|
};
|
|
match codec_id {
|
|
CODEC_ID_NONE => {
|
|
let ext_data = bits.extended_bitmap_data;
|
|
let source_width = destination.width();
|
|
let rectangle = match ext_data.bpp {
|
|
8 => image.apply_rgb8_with_palette(
|
|
ext_data.data,
|
|
&destination,
|
|
self.palette.colors(),
|
|
source_width,
|
|
)?,
|
|
15 => image.apply_rgb15_bitmap(ext_data.data, &destination, source_width)?,
|
|
16 => image.apply_rgb16_bitmap(ext_data.data, &destination, source_width)?,
|
|
24 => image.apply_bgr24_bitmap(ext_data.data, &destination, source_width)?,
|
|
32 => image.apply_rgb32_bitmap(
|
|
ext_data.data,
|
|
PixelFormat::BgrX32,
|
|
&destination,
|
|
source_width,
|
|
)?,
|
|
bpp => {
|
|
warn!("Unsupported surface CODEC_ID_NONE bpp: {bpp}");
|
|
continue;
|
|
}
|
|
};
|
|
update_rectangle = update_rectangle
|
|
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
|
.or(Some(rectangle));
|
|
}
|
|
CODEC_ID_REMOTEFX => {
|
|
let mut data = ReadCursor::new(bits.extended_bitmap_data.data);
|
|
while !data.is_empty() {
|
|
let (_frame_id, rectangle) = self.rfx_handler.decode(image, &destination, &mut data)?;
|
|
update_rectangle = update_rectangle
|
|
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
|
.or(Some(rectangle));
|
|
}
|
|
}
|
|
#[cfg(feature = "qoi")]
|
|
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => {
|
|
qoi_apply(
|
|
image,
|
|
destination,
|
|
bits.extended_bitmap_data.data,
|
|
&mut update_rectangle,
|
|
)?;
|
|
}
|
|
#[cfg(feature = "qoiz")]
|
|
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => {
|
|
let compressed = &bits.extended_bitmap_data.data;
|
|
let mut input = zstd_safe::InBuffer::around(compressed);
|
|
let mut data = vec![0; compressed.len() * 4];
|
|
let mut pos = 0;
|
|
loop {
|
|
let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos);
|
|
self.zdctx
|
|
.decompress_stream(&mut output, &mut input)
|
|
.map_err(zstd_safe::get_error_name)
|
|
.map_err(|e| reason_err!("zstd", "{}", e))?;
|
|
pos = output.pos();
|
|
if pos == output.capacity() {
|
|
data.resize(data.capacity() * 2, 0);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
qoi_apply(image, destination, &data, &mut update_rectangle)?;
|
|
}
|
|
_ => {
|
|
warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id);
|
|
}
|
|
}
|
|
}
|
|
SurfaceCommand::FrameMarker(marker) => {
|
|
trace!(
|
|
"Frame marker: action {:?} with ID #{}",
|
|
marker.frame_action,
|
|
marker.frame_id.unwrap_or(0)
|
|
);
|
|
self.marker_processor.process(&marker, output)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(update_rectangle.unwrap_or_else(InclusiveRectangle::empty))
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "qoi")]
|
|
fn qoi_apply(
|
|
image: &mut DecodedImage,
|
|
destination: InclusiveRectangle,
|
|
data: &[u8],
|
|
update_rectangle: &mut Option<InclusiveRectangle>,
|
|
) -> SessionResult<()> {
|
|
let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?;
|
|
|
|
// Guard against a decoded buffer that doesn't match the destination
|
|
// rectangle. `apply_rgb24`/`apply_rgba32` derive the row count from the
|
|
// decoded length, and the only bounds check downstream (`rect_fits`)
|
|
// validates the rectangle against the image, not the buffer against the
|
|
// rectangle. A malformed/oversized QOI payload would otherwise drive the
|
|
// per-row index past `self.data` and panic (client-side DoS).
|
|
let channels = match header.channels {
|
|
qoi::Channels::Rgb => 3,
|
|
qoi::Channels::Rgba => 4,
|
|
};
|
|
let expected = usize::from(destination.width()) * usize::from(destination.height()) * channels;
|
|
if decoded.len() != expected {
|
|
return Err(reason_err!(
|
|
"QOI decode",
|
|
"decoded {} bytes, expected {} for {}x{} ({} channels)",
|
|
decoded.len(),
|
|
expected,
|
|
destination.width(),
|
|
destination.height(),
|
|
channels
|
|
));
|
|
}
|
|
|
|
let rectangle = match header.channels {
|
|
qoi::Channels::Rgb => image.apply_rgb24(&decoded, &destination, destination.width(), false)?,
|
|
qoi::Channels::Rgba => image.apply_rgba32(&decoded, &destination, false)?,
|
|
};
|
|
|
|
*update_rectangle = update_rectangle
|
|
.as_ref()
|
|
.map(|rect: &InclusiveRectangle| rect.union(&rectangle))
|
|
.or(Some(rectangle));
|
|
Ok(())
|
|
}
|
|
|
|
pub struct ProcessorBuilder {
|
|
pub io_channel_id: u16,
|
|
pub user_channel_id: u16,
|
|
pub share_id: u32,
|
|
/// Ignore server pointer updates.
|
|
pub enable_server_pointer: bool,
|
|
/// Use software rendering mode for pointer bitmap generation. When this option is active,
|
|
/// `UpdateKind::PointerBitmap` will not be generated. Remote pointer will be drawn
|
|
/// via software rendering on top of the output image.
|
|
pub pointer_software_rendering: bool,
|
|
/// Bulk decompressor for server-to-client compressed PDUs.
|
|
/// `None` when compression was not negotiated.
|
|
pub bulk_decompressor: Option<BulkCompressor>,
|
|
}
|
|
|
|
impl ProcessorBuilder {
|
|
pub fn build(self) -> Processor {
|
|
Processor {
|
|
complete_data: CompleteData::new(),
|
|
rfx_handler: rfx::DecodingContext::new(),
|
|
marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id, self.share_id),
|
|
bitmap_stream_decoder: BitmapStreamDecoder::default(),
|
|
pointer_cache: PointerCache::default(),
|
|
use_system_pointer: true,
|
|
mouse_pos_update: None,
|
|
enable_server_pointer: self.enable_server_pointer,
|
|
pointer_software_rendering: self.pointer_software_rendering,
|
|
bulk_decompressor: self.bulk_decompressor,
|
|
palette: Palette::system_default(),
|
|
#[cfg(feature = "qoiz")]
|
|
zdctx: zstd_safe::DCtx::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
struct CompleteData {
|
|
fragmented_data: Option<Vec<u8>>,
|
|
}
|
|
|
|
impl CompleteData {
|
|
fn new() -> Self {
|
|
Self { fragmented_data: None }
|
|
}
|
|
|
|
fn process_data(&mut self, data: &[u8], fragmentation: Fragmentation) -> Option<Vec<u8>> {
|
|
match fragmentation {
|
|
Fragmentation::Single => {
|
|
self.check_data_is_empty();
|
|
|
|
Some(data.to_vec())
|
|
}
|
|
Fragmentation::First => {
|
|
self.check_data_is_empty();
|
|
|
|
self.fragmented_data = Some(data.to_vec());
|
|
|
|
None
|
|
}
|
|
Fragmentation::Next => {
|
|
self.append_data(data);
|
|
|
|
None
|
|
}
|
|
Fragmentation::Last => {
|
|
self.append_data(data);
|
|
|
|
self.fragmented_data.take()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_data_is_empty(&mut self) {
|
|
if self.fragmented_data.is_some() {
|
|
warn!("Skipping pending Fast-Path Update internal multiple elements data");
|
|
self.fragmented_data = None;
|
|
}
|
|
}
|
|
|
|
fn append_data(&mut self, data: &[u8]) {
|
|
if let Some(fragmented_data) = self.fragmented_data.as_mut() {
|
|
fragmented_data.extend_from_slice(data);
|
|
} else {
|
|
warn!("Got unexpected Next fragmentation PDU without prior First fragmentation PDU");
|
|
}
|
|
}
|
|
}
|
|
|
|
struct FrameMarkerProcessor {
|
|
user_channel_id: u16,
|
|
io_channel_id: u16,
|
|
share_id: u32,
|
|
}
|
|
|
|
impl FrameMarkerProcessor {
|
|
fn new(user_channel_id: u16, io_channel_id: u16, share_id: u32) -> Self {
|
|
Self {
|
|
user_channel_id,
|
|
io_channel_id,
|
|
share_id,
|
|
}
|
|
}
|
|
|
|
fn process(&mut self, marker: &FrameMarkerPdu, output: &mut WriteBuf) -> SessionResult<()> {
|
|
match marker.frame_action {
|
|
FrameAction::Begin => Ok(()),
|
|
FrameAction::End => {
|
|
ironrdp_pdu::rdp::headers::encode_share_data(
|
|
self.user_channel_id,
|
|
self.io_channel_id,
|
|
self.share_id,
|
|
ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu {
|
|
frame_id: marker.frame_id.unwrap_or(0),
|
|
}),
|
|
output,
|
|
)
|
|
.map_err(SessionError::encode)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|