feat(core): RDP client on IronRDP

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.
This commit is contained in:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+469
View File
@@ -0,0 +1,469 @@
use std::sync::Arc;
use ironrdp_bulk::BulkCompressor;
use ironrdp_core::{ReadCursor, WriteBuf};
use ironrdp_displaycontrol::client::DisplayControlClient;
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
use ironrdp_graphics::pointer::DecodedPointer;
use ironrdp_pdu::geometry::InclusiveRectangle;
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
use ironrdp_pdu::rdp::client_info::CompressionType as PduCompressionType;
use ironrdp_pdu::rdp::headers::ShareDataPdu;
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
use ironrdp_pdu::slow_path::{self, GraphicsUpdateType};
use ironrdp_pdu::{Action, mcs};
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages};
use tracing::{debug, info, warn};
use crate::fast_path::UpdateKind;
use crate::image::DecodedImage;
use crate::{SessionError, SessionErrorExt as _, SessionResult, fast_path, x224};
/// Converts the PDU-layer compression type to the bulk crate's compression type.
fn to_bulk_compression_type(ct: PduCompressionType) -> ironrdp_bulk::CompressionType {
match ct {
PduCompressionType::K8 => ironrdp_bulk::CompressionType::Rdp4,
PduCompressionType::K64 => ironrdp_bulk::CompressionType::Rdp5,
PduCompressionType::Rdp6 => ironrdp_bulk::CompressionType::Rdp6,
PduCompressionType::Rdp61 => ironrdp_bulk::CompressionType::Rdp61,
}
}
pub struct ActiveStage {
x224_processor: x224::Processor,
fast_path_processor: fast_path::Processor,
enable_server_pointer: bool,
}
/// Builder for [`ActiveStage`].
///
/// All fields are required; they are typically taken straight from `ironrdp-connector`’s
/// `ConnectionResult` once the connection sequence is finalized.
pub struct ActiveStageBuilder {
pub static_channels: StaticChannelSet,
pub user_channel_id: u16,
pub io_channel_id: u16,
pub message_channel_id: Option<u16>,
pub share_id: u32,
/// The bulk compression type that was negotiated, if any.
pub compression_type: Option<PduCompressionType>,
/// Enable server-side pointer updates (client-side pointer rendering).
pub enable_server_pointer: bool,
/// Use software rendering mode for pointer bitmap generation.
pub pointer_software_rendering: bool,
}
impl ActiveStageBuilder {
pub fn build(self) -> ActiveStage {
let Self {
static_channels,
user_channel_id,
io_channel_id,
message_channel_id,
share_id,
compression_type,
enable_server_pointer,
pointer_software_rendering,
} = self;
let x224_processor = x224::Processor::new(
static_channels,
user_channel_id,
io_channel_id,
message_channel_id,
share_id,
);
// Create bulk decompressor if compression was negotiated
let bulk_decompressor = compression_type.and_then(|ct| {
let bulk_ct = to_bulk_compression_type(ct);
match BulkCompressor::new(bulk_ct) {
Ok(compressor) => {
info!(compression_type = %bulk_ct, "Bulk decompressor initialized for FastPath");
Some(compressor)
}
Err(e) => {
tracing::error!(error = %e, "Failed to create bulk decompressor, compression disabled");
None
}
}
});
let fast_path_processor = fast_path::ProcessorBuilder {
io_channel_id,
user_channel_id,
share_id,
enable_server_pointer,
pointer_software_rendering,
bulk_decompressor,
}
.build();
ActiveStage {
x224_processor,
fast_path_processor,
enable_server_pointer,
}
}
}
impl ActiveStage {
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
self.fast_path_processor.update_mouse_pos(x, y);
}
/// Encodes outgoing input events and modifies image if necessary (e.g for client-side pointer
/// rendering).
pub fn process_fastpath_input(
&mut self,
image: &mut DecodedImage,
events: &[FastPathInputEvent],
) -> SessionResult<Vec<ActiveStageOutput>> {
if events.is_empty() {
return Ok(Vec::new());
}
// Mouse move events are prevalent, so we can preallocate space for
// response frame + graphics update
let mut output = Vec::with_capacity(2);
// Encoding fastpath response frame
// PERF: unnecessary copy
let fastpath_input = FastPathInput::new(events.to_vec()).map_err(SessionError::decode)?;
let frame = ironrdp_core::encode_vec(&fastpath_input).map_err(SessionError::encode)?;
output.push(ActiveStageOutput::ResponseFrame(frame));
// If pointer rendering is disabled - we can skip the rest
if !self.enable_server_pointer {
return Ok(output);
}
// If mouse was moved by client - we should update framebuffer to reflect new
// pointer position
let mouse_pos = events.iter().find_map(|event| match event {
FastPathInputEvent::MouseEvent(event) => Some((event.x_position, event.y_position)),
FastPathInputEvent::MouseEventEx(event) => Some((event.x_position, event.y_position)),
_ => None,
});
let (mouse_x, mouse_y) = match mouse_pos {
Some(mouse_pos) => mouse_pos,
None => return Ok(output),
};
// Graphics update is only sent when update is visually changed the framebuffer
if let Some(rect) = image.move_pointer(mouse_x, mouse_y)? {
output.push(ActiveStageOutput::GraphicsUpdate(rect));
}
Ok(output)
}
/// Process a frame received from the server.
pub fn process(
&mut self,
image: &mut DecodedImage,
action: Action,
frame: &[u8],
) -> SessionResult<Vec<ActiveStageOutput>> {
let (mut stage_outputs, processor_updates) = match action {
Action::FastPath => {
let mut output = WriteBuf::new();
let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?;
(
vec![ActiveStageOutput::ResponseFrame(output.into_inner())],
processor_updates,
)
}
Action::X224 => {
let x224_outputs = self.x224_processor.process(frame)?;
let mut stage_outputs = Vec::new();
let mut processor_updates = Vec::new();
for output in x224_outputs {
match output {
x224::ProcessorOutput::GraphicsUpdate(data) => {
let updates = process_slow_path_graphics(&mut self.fast_path_processor, image, &data)?;
processor_updates.extend(updates);
}
x224::ProcessorOutput::PointerUpdate(data) => {
let updates = process_slow_path_pointer(&mut self.fast_path_processor, image, &data)?;
processor_updates.extend(updates);
}
other => {
stage_outputs.push(ActiveStageOutput::try_from(other)?);
}
}
}
(stage_outputs, processor_updates)
}
};
for update in processor_updates {
match update {
UpdateKind::None => {}
UpdateKind::Region(region) => {
stage_outputs.push(ActiveStageOutput::GraphicsUpdate(region));
}
UpdateKind::PointerDefault => {
stage_outputs.push(ActiveStageOutput::PointerDefault);
}
UpdateKind::PointerHidden => {
stage_outputs.push(ActiveStageOutput::PointerHidden);
}
UpdateKind::PointerPosition { x, y } => {
stage_outputs.push(ActiveStageOutput::PointerPosition { x, y });
}
UpdateKind::PointerBitmap(pointer) => {
stage_outputs.push(ActiveStageOutput::PointerBitmap(pointer));
}
}
}
Ok(stage_outputs)
}
pub fn set_fastpath_processor(&mut self, processor: fast_path::Processor) {
self.fast_path_processor = processor;
}
/// Updates the share_id used by the x224 processor for encoding ShareDataPdu.
/// Must be called during Deactivation-Reactivation if the server assigns a new share_id.
pub fn set_share_id(&mut self, share_id: u32) {
self.x224_processor.set_share_id(share_id);
}
pub fn set_enable_server_pointer(&mut self, enable_server_pointer: bool) {
self.enable_server_pointer = enable_server_pointer;
}
/// Encodes client-side graceful shutdown request. Note that upon sending this request,
/// client should wait for server's ShutdownDenied PDU before closing the connection.
///
/// Client-side graceful shutdown is defined in [MS-RDPBCGR]
///
/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
pub fn graceful_shutdown(&self) -> SessionResult<Vec<ActiveStageOutput>> {
let mut frame = WriteBuf::new();
self.x224_processor
.encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?;
Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())])
}
/// Send a pdu on the static global channel. Typically used to send input events
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
self.x224_processor.encode_static(output, pdu)
}
pub fn get_svc_processor<T: SvcProcessor + 'static>(&mut self) -> Option<&T> {
self.x224_processor.get_svc_processor()
}
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
self.x224_processor.get_svc_processor_mut()
}
pub fn get_dvc<T: DvcProcessor + 'static>(&mut self) -> Option<&DynamicVirtualChannel> {
self.x224_processor.get_dvc::<T>()
}
pub fn get_dvc_by_channel_id(&mut self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
self.x224_processor.get_dvc_by_channel_id(channel_id)
}
/// Completes user's SVC request with data, required to sent it over the network and returns
/// a buffer with encoded data.
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
&self,
messages: SvcProcessorMessages<C>,
) -> SessionResult<Vec<u8>> {
self.x224_processor.process_svc_processor_messages(messages)
}
/// Fully encodes a resize request for sending over the Display Control Virtual Channel.
///
/// If the Display Control Virtual Channel is not available, or not yet connected, this method
/// will return `None`.
///
/// Per [2.2.2.2.1]:
/// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
/// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
/// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent.
/// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm.
///
/// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function
/// to ensure the display size is within the valid range.
///
/// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
pub fn encode_resize(
&mut self,
width: u32,
height: u32,
scale_factor: Option<u32>,
physical_dims: Option<(u32, u32)>,
) -> Option<SessionResult<Vec<u8>>> {
if let Some(dvc) = self.get_dvc::<DisplayControlClient>() {
if let Some(channel_id) = dvc.channel_id() {
let display_control = dvc.channel_processor_downcast_ref::<DisplayControlClient>()?;
let svc_messages = match display_control.encode_single_primary_monitor(
channel_id,
width,
height,
scale_factor,
physical_dims,
) {
Ok(messages) => messages,
Err(e) => return Some(Err(SessionError::encode(e))),
};
return Some(
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(svc_messages)),
);
} else {
debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected");
}
} else {
debug!("Could not encode a resize: Display Control Virtual Channel is not available");
}
None
}
pub fn encode_dvc_messages(&mut self, messages: Vec<SvcMessage>) -> SessionResult<Vec<u8>> {
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(messages))
}
}
#[derive(Debug)]
pub enum ActiveStageOutput {
ResponseFrame(Vec<u8>),
GraphicsUpdate(InclusiveRectangle),
PointerDefault,
PointerHidden,
PointerPosition {
x: u16,
y: u16,
},
PointerBitmap(Arc<DecodedPointer>),
Terminate(GracefulDisconnectReason),
/// Received a Server Deactivate All PDU. The consumer should execute the [Deactivation-Reactivation Sequence].
///
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
DeactivateAll,
/// Server Initiate Multitransport Request. The application should establish a
/// sideband UDP transport using the provided request parameters.
///
/// See [\[MS-RDPBCGR\] 2.2.15.1].
///
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
MultitransportRequest(MultitransportRequestPdu),
/// Server-reported network characteristics ([\[MS-RDPBCGR\] 2.2.14.1.5]).
///
/// Contains an [`AutoDetectRequest::NetworkCharacteristicsResult`] with
/// RTT and/or bandwidth measurements computed by the server.
///
/// See [\[MS-RDPBCGR\] 2.2.14.1.5].
///
/// [\[MS-RDPBCGR\] 2.2.14.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/228ffc5c-b60c-4d3e-9781-ac613f822fdf
AutoDetect(AutoDetectRequest),
}
impl TryFrom<x224::ProcessorOutput> for ActiveStageOutput {
type Error = SessionError;
fn try_from(value: x224::ProcessorOutput) -> Result<Self, Self::Error> {
match value {
x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)),
x224::ProcessorOutput::Disconnect(desc) => {
let desc = match desc {
x224::DisconnectDescription::McsDisconnect(reason) => match reason {
mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated,
mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated,
other => GracefulDisconnectReason::Other(other.description().to_owned()),
},
x224::DisconnectDescription::ErrorInfo(info) => GracefulDisconnectReason::Other(info.description()),
};
Ok(Self::Terminate(desc))
}
x224::ProcessorOutput::DeactivateAll => Ok(Self::DeactivateAll),
x224::ProcessorOutput::MultitransportRequest(pdu) => Ok(Self::MultitransportRequest(pdu)),
x224::ProcessorOutput::AutoDetect(request) => Ok(Self::AutoDetect(request)),
// GraphicsUpdate and PointerUpdate are consumed in ActiveStage::process()
// before reaching this conversion.
x224::ProcessorOutput::GraphicsUpdate(_) | x224::ProcessorOutput::PointerUpdate(_) => Err(
SessionError::general("slow-path graphics/pointer updates should be handled before this conversion"),
),
}
}
}
/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for
/// disconnect reasons.
#[derive(Debug, Clone)]
pub enum GracefulDisconnectReason {
UserInitiated,
ServerInitiated,
Other(String),
}
impl GracefulDisconnectReason {
pub fn description(&self) -> String {
match self {
GracefulDisconnectReason::UserInitiated => "user initiated disconnect".to_owned(),
GracefulDisconnectReason::ServerInitiated => "server initiated disconnect".to_owned(),
GracefulDisconnectReason::Other(description) => description.clone(),
}
}
}
impl core::fmt::Display for GracefulDisconnectReason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.description())
}
}
/// Parse and process a slow-path graphics update through the shared bitmap pipeline.
fn process_slow_path_graphics(
fast_path_processor: &mut fast_path::Processor,
image: &mut DecodedImage,
data: &[u8],
) -> SessionResult<Vec<UpdateKind>> {
let mut src = ReadCursor::new(data);
let update_type = slow_path::read_graphics_update_type(&mut src).map_err(SessionError::decode)?;
match update_type {
GraphicsUpdateType::Bitmap => {
let bitmap = slow_path::decode_slow_path_bitmap(&mut src).map_err(SessionError::decode)?;
fast_path_processor.process_bitmap_update(image, bitmap)
}
GraphicsUpdateType::Orders => {
warn!("Slow-path drawing orders not supported (MS-RDPEGDI)");
Ok(Vec::new())
}
GraphicsUpdateType::Palette => {
warn!("Slow-path palette update not supported (8bpp)");
Ok(Vec::new())
}
// Synchronize is an artifact from the T.128 multipoint protocol
// and carries no data. Safe to ignore.
GraphicsUpdateType::Synchronize => {
debug!("Ignoring slow-path synchronize update");
Ok(Vec::new())
}
}
}
/// Parse and process a slow-path pointer update through the shared pointer pipeline.
fn process_slow_path_pointer(
fast_path_processor: &mut fast_path::Processor,
image: &mut DecodedImage,
data: &[u8],
) -> SessionResult<Vec<UpdateKind>> {
let mut src = ReadCursor::new(data);
let pointer = slow_path::decode_slow_path_pointer(&mut src).map_err(SessionError::decode)?;
fast_path_processor.process_pointer_update(image, pointer)
}
+821
View File
@@ -0,0 +1,821 @@
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(())
}
}
}
}
File diff suppressed because it is too large Load Diff
+130
View File
@@ -0,0 +1,130 @@
#![cfg_attr(doc, doc = include_str!("../README.md"))]
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
mod macros;
pub mod fast_path;
pub mod image;
pub mod pointer;
pub mod rfx; // FIXME: maybe this module should not be in this crate
pub mod x224;
mod active_stage;
mod palette;
use core::fmt;
pub use active_stage::{ActiveStage, ActiveStageBuilder, ActiveStageOutput, GracefulDisconnectReason};
pub type SessionResult<T> = Result<T, SessionError>;
#[non_exhaustive]
#[derive(Debug)]
pub enum SessionErrorKind {
Pdu(ironrdp_pdu::PduError),
Encode(ironrdp_core::EncodeError),
Decode(ironrdp_core::DecodeError),
Reason(String),
General,
Custom,
}
impl fmt::Display for SessionErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
SessionErrorKind::Pdu(_) => write!(f, "PDU error"),
SessionErrorKind::Encode(_) => write!(f, "encode error"),
SessionErrorKind::Decode(_) => write!(f, "decode error"),
SessionErrorKind::Reason(description) => write!(f, "reason: {description}"),
SessionErrorKind::General => write!(f, "general error"),
SessionErrorKind::Custom => write!(f, "custom error"),
}
}
}
impl core::error::Error for SessionErrorKind {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match &self {
SessionErrorKind::Pdu(e) => Some(e),
SessionErrorKind::Encode(e) => Some(e),
SessionErrorKind::Decode(e) => Some(e),
SessionErrorKind::Reason(_) => None,
SessionErrorKind::General => None,
SessionErrorKind::Custom => None,
}
}
}
pub type SessionError = ironrdp_error::Error<SessionErrorKind>;
pub trait SessionErrorExt {
fn pdu(error: ironrdp_pdu::PduError) -> Self;
fn encode(error: ironrdp_core::EncodeError) -> Self;
fn decode(error: ironrdp_core::DecodeError) -> Self;
fn general(context: &'static str) -> Self;
fn reason(context: &'static str, reason: impl Into<String>) -> Self;
fn custom<E>(context: &'static str, e: E) -> Self
where
E: core::error::Error + Sync + Send + 'static;
}
impl SessionErrorExt for SessionError {
#[track_caller]
fn pdu(error: ironrdp_pdu::PduError) -> Self {
Self::new("payload error", SessionErrorKind::Pdu(error))
}
#[track_caller]
fn encode(error: ironrdp_core::EncodeError) -> Self {
Self::new("encode error", SessionErrorKind::Encode(error))
}
#[track_caller]
fn decode(error: ironrdp_core::DecodeError) -> Self {
Self::new("decode error", SessionErrorKind::Decode(error))
}
#[track_caller]
fn general(context: &'static str) -> Self {
Self::new(context, SessionErrorKind::General)
}
#[track_caller]
fn reason(context: &'static str, reason: impl Into<String>) -> Self {
Self::new(context, SessionErrorKind::Reason(reason.into()))
}
#[track_caller]
fn custom<E>(context: &'static str, e: E) -> Self
where
E: core::error::Error + Sync + Send + 'static,
{
Self::new(context, SessionErrorKind::Custom).with_source(e)
}
}
pub trait SessionResultExt {
#[must_use]
fn with_context(self, context: &'static str) -> Self;
#[must_use]
fn with_source<E>(self, source: E) -> Self
where
E: core::error::Error + Sync + Send + 'static;
}
impl<T> SessionResultExt for SessionResult<T> {
fn with_context(self, context: &'static str) -> Self {
self.map_err(|mut e| {
e.set_context(context);
e
})
}
fn with_source<E>(self, source: E) -> Self
where
E: core::error::Error + Sync + Send + 'static,
{
self.map_err(|e| e.with_source(source))
}
}
+57
View File
@@ -0,0 +1,57 @@
/// Creates a `SessionError` with `General` kind
///
/// Shorthand for
/// ```ignore
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::general(context)
/// ```
#[macro_export]
macro_rules! general_err {
( $context:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::general($context) }};
}
/// Creates a `SessionError` with `Reason` kind
///
/// Shorthand for
/// ```ignore
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::reason(context, reason)
/// ```
#[macro_export]
macro_rules! reason_err {
( $context:expr, $($arg:tt)* ) => {{
<$crate::SessionError as $crate::SessionErrorExt>::reason($context, format!($($arg)*))
}};
}
/// Creates a `SessionError` with `Custom` kind and a source error attached to it
///
/// Shorthand for
/// ```ignore
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::custom(context, source)
/// ```
#[macro_export]
macro_rules! custom_err {
( $context:expr, $source:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::custom($context, $source) }};
}
#[macro_export]
macro_rules! eof_try {
($e:expr) => {
match $e {
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
return Ok(None);
}
result => result,
}
};
}
#[macro_export]
macro_rules! try_ready {
($e:expr) => {
match $e {
Ok(Some(v)) => Ok(v),
Ok(None) => return Ok(None),
Err(e) => Err(e),
}
};
}
+89
View File
@@ -0,0 +1,89 @@
use tracing::{debug, warn};
/// 8bpp color palette (256 RGB entries).
///
/// Initialized with the default Windows system palette (VGA colors)
/// per MS-RDPBCGR 2.2.9.1.1.3.1.1. Updated by TS_UPDATE_PALETTE_DATA
/// fast-path updates during the session.
#[derive(Debug, Clone)]
pub(crate) struct Palette {
colors: [[u8; 3]; 256],
}
impl Palette {
/// Create a palette initialized with the 20 static colors from the
/// Windows default system palette. Indices 0-9 and 246-255 are the
/// reserved static colors; the middle 236 entries (10-245) are black.
///
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/gdi/default-palette>
pub(crate) fn system_default() -> Self {
let mut colors = [[0u8; 3]; 256];
// Lower 10 static colors (indices 0-9)
colors[0] = [0, 0, 0]; // Black
colors[1] = [128, 0, 0]; // Dark Red
colors[2] = [0, 128, 0]; // Dark Green
colors[3] = [128, 128, 0]; // Dark Yellow
colors[4] = [0, 0, 128]; // Dark Blue
colors[5] = [128, 0, 128]; // Dark Magenta
colors[6] = [0, 128, 128]; // Dark Cyan
colors[7] = [192, 192, 192]; // Light Gray
colors[8] = [192, 220, 192]; // Money Green
colors[9] = [166, 202, 240]; // Sky Blue
// Upper 10 static colors (indices 246-255)
colors[246] = [255, 251, 240]; // Cream
colors[247] = [160, 160, 164]; // Medium Gray
colors[248] = [128, 128, 128]; // Dark Gray
colors[249] = [255, 0, 0]; // Red
colors[250] = [0, 255, 0]; // Green
colors[251] = [255, 255, 0]; // Yellow
colors[252] = [0, 0, 255]; // Blue
colors[253] = [255, 0, 255]; // Magenta
colors[254] = [0, 255, 255]; // Cyan
colors[255] = [255, 255, 255]; // White
Self { colors }
}
/// Parse TS_UPDATE_PALETTE_DATA and update palette entries.
/// Wire format: pad(2) + numberColors(u32) + N x TS_COLOR_QUAD [B, G, R, pad].
pub(crate) fn process_update(&mut self, data: &[u8]) {
if data.len() < 6 {
warn!("Palette update too short: {} bytes", data.len());
return;
}
let raw_count = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
// Palette can have at most 256 entries; clamp before any arithmetic
// to prevent overflow on untrusted input
let clamped = raw_count.min(256);
let number_colors = usize::try_from(clamped).unwrap_or(256);
let entry_data = &data[6..];
let Some(required_len) = number_colors.checked_mul(4) else {
warn!("Palette entry count overflow");
return;
};
if entry_data.len() < required_len {
warn!(
"Palette data truncated: expected {} bytes for {} colors, got {}",
required_len,
number_colors,
entry_data.len()
);
return;
}
for i in 0..number_colors {
let offset = i * 4;
// TS_COLOR_QUAD: Blue, Green, Red, Pad
self.colors[i] = [entry_data[offset + 2], entry_data[offset + 1], entry_data[offset]];
}
debug!("Updated palette with {} colors", number_colors);
}
/// Borrow the underlying color table for bitmap application.
pub(crate) fn colors(&self) -> &[[u8; 3]; 256] {
&self.colors
}
}
+24
View File
@@ -0,0 +1,24 @@
use std::collections::HashMap;
use std::sync::Arc;
use ironrdp_graphics::pointer::DecodedPointer;
#[derive(Debug, Clone, Default)]
pub struct PointerCache {
// TODO(@pacancoder) maybe use Vec<Optional<...>> instead?
cache: HashMap<usize, Arc<DecodedPointer>>,
}
impl PointerCache {
pub fn insert(&mut self, id: usize, pointer: Arc<DecodedPointer>) -> Option<Arc<DecodedPointer>> {
self.cache.insert(id, pointer)
}
pub fn get(&self, id: usize) -> Option<Arc<DecodedPointer>> {
self.cache.get(&id).cloned()
}
pub fn is_cached(&self, id: usize) -> bool {
self.cache.contains_key(&id)
}
}
+286
View File
@@ -0,0 +1,286 @@
use core::cmp::min;
use ironrdp_graphics::color_conversion::{self, YCbCrBuffer};
use ironrdp_graphics::image_processing::PixelFormat;
use ironrdp_graphics::rectangle_processing::Region;
use ironrdp_graphics::{dwt, quantization, rlgr, subband_reconstruction};
use ironrdp_pdu::codecs::rfx::{self, EntropyAlgorithm, Quant, RfxRectangle, Tile};
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
use ironrdp_pdu::{Decode as _, ReadCursor, decode_cursor};
use tracing::{instrument, trace};
use crate::image::DecodedImage;
use crate::{SessionResult, custom_err, general_err, reason_err};
const TILE_SIZE: u16 = 64;
pub type FrameId = u32;
pub struct DecodingContext {
context: rfx::ContextPdu,
channels: rfx::ChannelsPdu,
decoding_tiles: DecodingTileContext,
}
impl Default for DecodingContext {
fn default() -> Self {
Self {
context: rfx::ContextPdu {
flags: rfx::OperatingMode::empty(),
entropy_algorithm: EntropyAlgorithm::Rlgr1,
},
channels: rfx::ChannelsPdu(Vec::new()),
decoding_tiles: DecodingTileContext::new(),
}
}
}
impl DecodingContext {
pub fn new() -> Self {
Self::default()
}
pub fn decode(
&mut self,
image: &mut DecodedImage,
destination: &InclusiveRectangle,
input: &mut ReadCursor<'_>,
) -> SessionResult<(FrameId, InclusiveRectangle)> {
loop {
let block = rfx::Block::decode(input).map_err(|e| custom_err!("decode block", e))?;
match block {
rfx::Block::Sync(_) => {
self.process_sync(input)?;
}
rfx::Block::CodecChannel(rfx::CodecChannel::FrameBegin(f)) => {
return self.process_frame(f, input, image, destination);
}
_ => {
return Err(reason_err!(
"rfx::DecodingContext",
"unexpected RFX block type: {:?}",
block.block_type()
));
}
}
}
}
fn process_sync(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
self.process_headers(input)
}
fn process_headers(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
let mut context = None;
let mut channels = None;
// headers can appear in any order: CodecVersions, Channels, Context
for _ in 0..3 {
match decode_cursor(input).map_err(|e| custom_err!("decode headers", e))? {
rfx::Block::CodecChannel(rfx::CodecChannel::Context(c)) => context = Some(c),
rfx::Block::Channels(c) => channels = Some(c),
rfx::Block::CodecVersions(_) => (),
_ => {
return Err(general_err!("unexpected RFX block type"));
}
}
}
let context = context.ok_or_else(|| general_err!("context header is missing"))?;
let channels = channels.ok_or_else(|| general_err!("channels header is missing"))?;
if channels.0.is_empty() {
return Err(general_err!("no RFX channel announced"));
}
self.context = context;
self.channels = channels;
Ok(())
}
#[instrument(skip_all)]
fn process_frame(
&mut self,
frame_begin: rfx::FrameBeginPdu,
input: &mut ReadCursor<'_>,
image: &mut DecodedImage,
destination: &InclusiveRectangle,
) -> SessionResult<(FrameId, InclusiveRectangle)> {
let channel = self
.channels
.0
.first()
.ok_or_else(|| general_err!("no RFX channel found"))?;
let width = channel.width.try_into().map_err(|_| general_err!("invalid width"))?;
let height = channel.height.try_into().map_err(|_| general_err!("invalid height"))?;
let entropy_algorithm = self.context.entropy_algorithm;
let region: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode region", e))?;
let mut region = match region {
rfx::Block::CodecChannel(rfx::CodecChannel::Region(region)) => region,
_ => return Err(general_err!("unexpected block type")),
};
let tile_set: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode tile_set", e))?;
let tile_set = match tile_set {
rfx::Block::CodecChannel(rfx::CodecChannel::TileSet(t)) => t,
_ => return Err(general_err!("unexpected block type")),
};
let frame_end: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode frame_end", e))?;
if !matches!(frame_end, rfx::Block::CodecChannel(rfx::CodecChannel::FrameEnd(_))) {
return Err(general_err!("unexpected block type"));
}
if region.rectangles.is_empty() {
region.rectangles = vec![RfxRectangle {
x: 0,
y: 0,
width,
height,
}];
}
let region = region;
trace!(frame_index = frame_begin.index);
trace!(destination_rectangle = ?destination);
trace!(context = ?self.context);
trace!(channels = ?self.channels);
trace!(?region);
let clipping_rectangles = clipping_rectangles(region.rectangles.as_slice(), destination, width, height);
trace!("Clipping rectangles: {:?}", clipping_rectangles);
let mut final_update_rectangle = clipping_rectangles.extents.clone();
for (update_rectangle, tile_data) in tiles_to_rectangles(tile_set.tiles.as_slice(), destination)
.zip(map_tiles_data(tile_set.tiles.as_slice(), tile_set.quants.as_slice()))
{
decode_tile(
&tile_data,
entropy_algorithm,
self.decoding_tiles.tile_output.as_mut(),
self.decoding_tiles.ycbcr_buffer.as_mut(),
self.decoding_tiles.ycbcr_temp_buffer.as_mut(),
)?;
let current_update_rectangle = image.apply_tile(
&self.decoding_tiles.tile_output,
PixelFormat::RgbA32,
&clipping_rectangles,
&update_rectangle,
)?;
final_update_rectangle = final_update_rectangle.union(&current_update_rectangle);
}
Ok((frame_begin.index, final_update_rectangle))
}
}
#[derive(Debug, Clone)]
struct DecodingTileContext {
tile_output: Vec<u8>,
ycbcr_buffer: Vec<Vec<i16>>,
ycbcr_temp_buffer: Vec<i16>,
}
impl DecodingTileContext {
fn new() -> Self {
let tile_size = usize::from(TILE_SIZE);
Self {
tile_output: vec![0; tile_size * tile_size * 4],
ycbcr_buffer: vec![vec![0; tile_size * tile_size]; 3],
ycbcr_temp_buffer: vec![0; tile_size * tile_size],
}
}
}
fn decode_tile(
tile: &TileData<'_>,
entropy_algorithm: EntropyAlgorithm,
output: &mut [u8],
ycbcr_temp: &mut [Vec<i16>],
temp: &mut [i16],
) -> SessionResult<()> {
for ((quant, data), ycbcr_buffer) in tile.quants.iter().zip(tile.data.iter()).zip(ycbcr_temp.iter_mut()) {
decode_component(quant, entropy_algorithm, data, ycbcr_buffer.as_mut_slice(), temp)?;
}
let ycbcr_buffer = YCbCrBuffer {
y: ycbcr_temp[0].as_slice(),
cb: ycbcr_temp[1].as_slice(),
cr: ycbcr_temp[2].as_slice(),
};
color_conversion::ycbcr_to_rgba(ycbcr_buffer, output).map_err(|e| custom_err!("decode_tile", e))?;
Ok(())
}
fn decode_component(
quant: &Quant,
entropy_algorithm: EntropyAlgorithm,
data: &[u8],
output: &mut [i16],
temp: &mut [i16],
) -> SessionResult<()> {
rlgr::decode(entropy_algorithm, data, output).map_err(|e| custom_err!("decode_component", e))?;
subband_reconstruction::decode(&mut output[4032..]);
quantization::decode(output, quant);
dwt::decode(output, temp);
Ok(())
}
fn clipping_rectangles(
rectangles: &[RfxRectangle],
destination: &InclusiveRectangle,
width: u16,
height: u16,
) -> Region {
let mut clipping_rectangles = Region::new();
rectangles
.iter()
.map(|r| InclusiveRectangle {
left: min(destination.left + r.x, width - 1),
top: min(destination.top + r.y, height - 1),
right: min(destination.left + r.x + r.width - 1, width - 1),
bottom: min(destination.top + r.y + r.height - 1, height - 1),
})
.for_each(|r| clipping_rectangles.union_rectangle(r));
clipping_rectangles
}
fn tiles_to_rectangles<'a>(
tiles: &'a [Tile<'_>],
destination: &'a InclusiveRectangle,
) -> impl Iterator<Item = InclusiveRectangle> + 'a {
tiles.iter().map(|t| InclusiveRectangle {
left: destination.left + t.x * TILE_SIZE,
top: destination.top + t.y * TILE_SIZE,
right: destination.left + t.x * TILE_SIZE + TILE_SIZE - 1,
bottom: destination.top + t.y * TILE_SIZE + TILE_SIZE - 1,
})
}
fn map_tiles_data<'a>(tiles: &[Tile<'a>], quants: &[Quant]) -> Vec<TileData<'a>> {
tiles
.iter()
.map(|t| TileData {
quants: [
quants[usize::from(t.y_quant_index)].clone(),
quants[usize::from(t.cb_quant_index)].clone(),
quants[usize::from(t.cr_quant_index)].clone(),
],
data: [t.y_data, t.cb_data, t.cr_data],
})
.collect()
}
struct TileData<'a> {
quants: [Quant; 3],
data: [&'a [u8]; 3],
}
+290
View File
@@ -0,0 +1,290 @@
use ironrdp_core::{WriteBuf, decode};
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage, SendDataIndicationCtx};
use ironrdp_pdu::rdp::autodetect::{AutoDetectReqPdu, AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
use ironrdp_pdu::rdp::headers::ShareDataPdu;
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
use ironrdp_pdu::x224::X224;
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages, client_encode_svc_messages};
use tracing::debug;
use crate::{SessionError, SessionErrorExt as _, SessionResult, reason_err};
/// X224 Processor output
#[derive(Debug, Clone)]
pub enum ProcessorOutput {
/// A buffer with encoded data to send to the server.
ResponseFrame(Vec<u8>),
/// A graceful disconnect notification. Client should close the connection upon receiving this.
Disconnect(DisconnectDescription),
/// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the
/// [Deactivation-Reactivation Sequence].
///
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
DeactivateAll,
/// Server Initiate Multitransport Request. The application should establish a
/// sideband UDP transport using the request ID and security cookie, then send
/// a [`MultitransportResponsePdu`] back on the IO channel.
///
/// See [\[MS-RDPBCGR\] 2.2.15.1].
///
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
/// [`MultitransportResponsePdu`]: ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu
MultitransportRequest(MultitransportRequestPdu),
/// Auto-detect network characteristics from server ([\[MS-RDPBCGR\] 2.2.14]).
///
/// Currently only surfaces [`AutoDetectRequest::NetworkCharacteristicsResult`].
/// RTT requests are handled internally with automatic responses.
///
/// [\[MS-RDPBCGR\] 2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dc672839-4f4e-40b1-a71c-cd6a959baa38
AutoDetect(AutoDetectRequest),
/// Slow-path graphics update ([MS-RDPBCGR] 2.2.9.1.1.3).
/// Raw update payload starting with `updateType(u16)`.
GraphicsUpdate(Vec<u8>),
/// Slow-path pointer update ([MS-RDPBCGR] 2.2.9.1.1.4).
/// Raw pointer payload starting with `messageType(u16) + pad(u16)`.
PointerUpdate(Vec<u8>),
}
#[derive(Debug, Clone)]
pub enum DisconnectDescription {
/// Includes the reason from the MCS Disconnect Provider Ultimatum.
/// This is the least-specific disconnect reason and is only used
/// when a more specific disconnect code is not available.
McsDisconnect(DisconnectReason),
/// Includes the error information sent by the RDP server when there
/// is a connection or disconnection failure.
ErrorInfo(ErrorInfo),
}
pub struct Processor {
static_channels: StaticChannelSet,
user_channel_id: u16,
io_channel_id: u16,
message_channel_id: Option<u16>,
share_id: u32,
}
impl Processor {
pub fn new(
static_channels: StaticChannelSet,
user_channel_id: u16,
io_channel_id: u16,
message_channel_id: Option<u16>,
share_id: u32,
) -> Self {
Self {
static_channels,
user_channel_id,
io_channel_id,
message_channel_id,
share_id,
}
}
pub fn set_share_id(&mut self, share_id: u32) {
self.share_id = share_id;
}
pub fn get_svc_processor<T: SvcProcessor + 'static>(&self) -> Option<&T> {
self.static_channels
.get_by_type::<T>()
.and_then(|svc| svc.channel_processor_downcast_ref())
}
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
self.static_channels
.get_by_type_mut::<T>()
.and_then(|svc| svc.channel_processor_downcast_mut())
}
/// Completes user's SVC request with data, required to sent it over the network and returns
/// a buffer with encoded data.
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
&self,
messages: SvcProcessorMessages<C>,
) -> SessionResult<Vec<u8>> {
let channel_id = self
.static_channels
.get_channel_id_by_type::<C>()
.ok_or_else(|| reason_err!("SVC", "channel not found"))?;
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
}
pub fn get_dvc<T: DvcProcessor + 'static>(&self) -> Option<&DynamicVirtualChannel> {
self.get_svc_processor::<DrdynvcClient>()?.get_dvc_by_type_id::<T>()
}
pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
self.get_svc_processor::<DrdynvcClient>()?
.get_dvc_by_channel_id(channel_id)
}
/// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed
/// in the returned order.
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<ProcessorOutput>> {
let data_ctx: SendDataIndicationCtx<'_> =
ironrdp_pdu::mcs::decode_send_data_indication(frame).map_err(SessionError::decode)?;
let channel_id = data_ctx.channel_id;
if channel_id == self.io_channel_id {
self.process_io_channel(data_ctx)
} else if self.message_channel_id == Some(channel_id) {
self.process_message_channel(data_ctx)
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?;
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
} else {
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
}
}
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
let io_channel = ironrdp_pdu::rdp::headers::decode_io_channel(data_ctx).map_err(SessionError::decode)?;
match io_channel {
ironrdp_pdu::rdp::headers::IoChannelPdu::Data(ctx) => {
match ctx.pdu {
ShareDataPdu::SaveSessionInfo(session_info) => {
debug!("Got Session Save Info PDU: {session_info:?}");
Ok(Vec::new())
}
// FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU"
ShareDataPdu::SetKeyboardIndicators(data) => {
debug!("Got Keyboard Indicators PDU: {data:?}");
Ok(Vec::new())
}
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
ProtocolIndependentCode::None,
))) => {
debug!("Received None server error");
Ok(Vec::new())
}
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
// This is a part of server-side graceful disconnect procedure defined
// in [MS-RDPBCGR].
//
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8
let desc = DisconnectDescription::ErrorInfo(e);
Ok(vec![ProcessorOutput::Disconnect(desc)])
}
ShareDataPdu::ShutdownDenied => {
debug!("ShutdownDenied received, session will be closed");
// As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we
// need to send a disconnect ultimatum to the server if we want to proceed with the
// session shutdown.
//
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
let ultimatum = McsMessage::DisconnectProviderUltimatum(
DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested),
);
let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode);
Ok(vec![
ProcessorOutput::ResponseFrame(encoded_pdu?),
ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect(
DisconnectReason::UserRequested,
)),
])
}
// TODO: slow-path payloads may be bulk-compressed when
// ClientInfoFlags::COMPRESSION is negotiated. Decompression
// should happen here before passing data downstream. Currently
// IronRDP does not wire bulk decompression into this path.
// FIXME: until this is wired, the client deliberately defaults to the simple,
// stateless-friendly MPPC 64K (RDP5) compression level rather than XCRUSH; a
// stateful codec would risk silent corruption on slow-path updates.
ShareDataPdu::Update(data) => {
debug!("Got slow-path graphics update ({} bytes)", data.len());
Ok(vec![ProcessorOutput::GraphicsUpdate(data)])
}
ShareDataPdu::Pointer(data) => {
debug!("Got slow-path pointer update ({} bytes)", data.len());
Ok(vec![ProcessorOutput::PointerUpdate(data)])
}
_ => Err(reason_err!(
"IO channel",
"unhandled PDU: {:?}",
ctx.pdu.as_short_name()
)),
}
}
ironrdp_pdu::rdp::headers::IoChannelPdu::MultitransportRequest(pdu) => {
debug!(
"Received Initiate Multitransport Request: request_id={}",
pdu.request_id
);
Ok(vec![ProcessorOutput::MultitransportRequest(pdu)])
}
ironrdp_pdu::rdp::headers::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll]),
}
}
/// Process an auto-detect request received on the MCS message channel.
///
/// During continuous auto-detection ([MS-RDPBCGR] 2.2.14) the server sends
/// RTT (and bandwidth) requests on the message channel; the client answers
/// RTT requests and surfaces the final Network Characteristics Result.
fn process_message_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
let Some(message_channel_id) = self.message_channel_id else {
return Err(reason_err!("message channel", "no message channel negotiated"));
};
let req = decode::<AutoDetectReqPdu>(data_ctx.user_data).map_err(SessionError::decode)?;
match req.request {
AutoDetectRequest::RttRequest { sequence_number, .. } => {
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
let mut frame = WriteBuf::new();
ironrdp_pdu::mcs::encode_send_data_request(
self.user_channel_id,
message_channel_id,
&response,
&mut frame,
)
.map_err(SessionError::encode)?;
debug!(sequence_number, "Responded to auto-detect RTT request");
Ok(vec![ProcessorOutput::ResponseFrame(frame.into_inner())])
}
req @ AutoDetectRequest::NetworkCharacteristicsResult { .. } => {
debug!(?req, "Received network characteristics from server");
Ok(vec![ProcessorOutput::AutoDetect(req)])
}
req => {
debug!(?req, "Auto-detect request not yet implemented");
Ok(Vec::new())
}
}
}
/// Send a pdu on the static global channel. Typically used to send input events
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
let written = ironrdp_pdu::rdp::headers::encode_share_data(
self.user_channel_id,
self.io_channel_id,
self.share_id,
pdu,
output,
)
.map_err(SessionError::encode)?;
Ok(written)
}
}
/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel.
///
/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer.
/// The messages returned here are ready to be sent to the server.
///
/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel.
fn process_svc_messages(messages: Vec<SvcMessage>, channel_id: u16, initiator_id: u16) -> SessionResult<Vec<u8>> {
client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode)
}