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:
+469
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1038
File diff suppressed because it is too large
Load Diff
+130
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(¤t_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
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user