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
+214
View File
@@ -0,0 +1,214 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.10.0...ironrdp-session-v0.11.0)] - 2026-07-10
### <!-- 0 -->Security
- [**breaking**] Send NetworkAutoDetect over the MCS message channel ([#1348](https://github.com/Devolutions/IronRDP/issues/1348)) ([8a1fd0118e](https://github.com/Devolutions/IronRDP/commit/8a1fd0118e0bac214c9050b6ca6b36a040046dd3))
Corrects Network Auto-Detect framing and routing to match MS-RDPBCGR by
moving it off the I/O channel slow-path Share Data PDUs and onto the MCS
message channel with the required Basic Security Header
(SEC_AUTODETECT_REQ / SEC_AUTODETECT_RSP). This aligns IronRDP with
mstsc/xfreerdp behavior and enables both connect-time and continuous
auto-detection to actually function.
### <!-- 4 -->Bug Fixes
- Propagate caller location through error constructor helpers ([#1392](https://github.com/Devolutions/IronRDP/issues/1392)) ([d6990d81a1](https://github.com/Devolutions/IronRDP/commit/d6990d81a17e8349e52768ad8a82f673b1e1462d))
The error constructor helpers in several crates wrap the #[track_caller]
ironrdp_error::Error::new, but were not themselves marked
#[track_caller]. As a result, the captured location pointed at the
helper body instead of the real call site, giving misleading "@
file:line" info in error reports.
- Reduce dependency on ironrdp-connector ([#1419](https://github.com/Devolutions/IronRDP/issues/1419)) ([5c22f86a71](https://github.com/Devolutions/IronRDP/commit/5c22f86a7150bc10c26a3be39bfaebf84c67d781))
Drops session's reliance on ironrdp-connector legacy helpers, now sourced from ironrdp-pdu.
- [**breaking**] Remove ironrdp-connector dependency ([#1435](https://github.com/Devolutions/IronRDP/issues/1435)) ([c6a0286dcb](https://github.com/Devolutions/IronRDP/commit/c6a0286dcb49d9ac54c65c4f9325b41e05d541b8))
Removes the last ironrdp-connector coupling from ironrdp-session by
turning Deactivate-All handling into a bare signal and shifting ownership
of the Deactivation-Reactivation activation sequence back to each consumer.
It introduces a ConnectionActivationFactory (fresh sequence per reactivation)
and an ActiveStageBuilder so session construction no longer depends on
ConnectionResult.
## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.9.0...ironrdp-session-v0.10.0)] - 2026-06-05
### <!-- 4 -->Bug Fixes
- Decode RGBA QOI bitmaps instead of dropping the frame ([#1341](https://github.com/Devolutions/IronRDP/issues/1341)) ([ef20ea4e90](https://github.com/Devolutions/IronRDP/commit/ef20ea4e90455d6c6db0d3521f6522d1e960c0bb))
Fixes the client-side QOI decode path in ironrdp-session so RGBA-channel QOI frames are decoded and applied to the framebuffer instead of being dropped, improving interoperability with third-party RDP servers and older ironrdp-server builds that emit RGBA QOI.
### <!-- 7 -->Build
- [**breaking**] Update `ironrdp-dvc` public dependency
## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.8.0...ironrdp-session-v0.9.0)] - 2026-05-27
### <!-- 1 -->Features
- Dispatch multitransport PDUs on IO channel ([#1096](https://github.com/Devolutions/IronRDP/issues/1096)) ([7853e3cc6f](https://github.com/Devolutions/IronRDP/commit/7853e3cc6f26acaf3da000c6177ca3cef6ef85fd))
`decode_io_channel()` assumes all IO channel PDUs begin with
a`ShareControlHeader`. Multitransport Request PDUs use a
`BasicSecurityHeader` with `SEC_TRANSPORT_REQ` instead ([MS-RDPBCGR]
2.2.15.1).
This adds a peek-based dispatch: check the first `u16`
for`TRANSPORT_REQ`, decode as `MultitransportRequestPdu` if set,
otherwise fall through to the existing `decode_share_control()` path
unchanged.
The new variant is propagated through `ProcessorOutput` and
'ActiveStageOutput` so applications can handle multitransport requests.
Client and web consumers log the request (no UDP transport yet).
- Add bulk compression and wire negotiation ([ebf5da5f33](https://github.com/Devolutions/IronRDP/commit/ebf5da5f3380a3355f6c95814d669f8190425ded))
- add ironrdp-bulk crate with MPPC/NCRUSH/XCRUSH, bitstream, benches, and metrics
- advertise compression in Client Info and plumb compression_type through connector
- decode compressed FastPath/ShareData updates using BulkCompressor
- update CLI to numeric compression flags (enabled by default, level 0-3)
- extend screenshot example with compression options and negotiated logging
- refresh tests, FFI/web configs, typos, and Cargo.lock
- Complete pixel format support for bitmap updates ([#1134](https://github.com/Devolutions/IronRDP/issues/1134)) ([a6b41093ce](https://github.com/Devolutions/IronRDP/commit/a6b41093ce4ece081d2538c157f6bc547c3b2607))
Wires missing bitmap pixel formats (8/15/24bpp) into the session rendering
pipeline so bitmap updates at those depths are rendered instead of being
dropped, and adds fast-path palette update parsing to support 8bpp indexed
color sessions.
- Handle Auto-Detect Request PDUs from server ([#1178](https://github.com/Devolutions/IronRDP/issues/1178)) ([4dcad09980](https://github.com/Devolutions/IronRDP/commit/4dcad09980e4f5354e4e435a134cc0956e2fcf9e))
Fixes a crash when the server sends Auto-Detect Request PDUs during an
active session. After #1176 added ShareDataPdu::AutoDetectReq routing,
these PDUs decode correctly but hit the catch-all error path in the x224
processor: "unhandled PDU: Auto-Detect Request PDU".
- Handle slow-path graphics and pointer updates ([#1132](https://github.com/Devolutions/IronRDP/issues/1132)) ([9383380292](https://github.com/Devolutions/IronRDP/commit/938338029290f1be82a7f784d544bb77ac797aeb))
Adds support for slow-path graphics and pointer updates to IronRDP, fixing connectivity issues with servers like XRDP that use slow-path output instead of fast-path. The implementation parses slow-path framing headers and routes the inner payload structures through the existing fast-path processing pipeline by extracting shared bitmap and pointer processing methods.
### <!-- 4 -->Bug Fixes
- Fix pixel format handling in bitmap decoders ([#1101](https://github.com/Devolutions/IronRDP/issues/1101)) ([75863245ab](https://github.com/Devolutions/IronRDP/commit/75863245ab376f15e35c00df434860c93b123633))
- Handle row padding in uncompressed bitmap updates ([4262ae75ff](https://github.com/Devolutions/IronRDP/commit/4262ae75ffa5cb1fabb4ca07d598e33d855e8fdd))
Uncompressed bitmap data has rows padded to 4-byte boundaries per
[MS-RDPBCGR] 2.2.9.1.1.3.1.2.2, but the bitmap apply functions
expect tightly packed pixel data. Strip the per-row padding before
passing raw bitmap data to the apply functions.
This fixes garbled bitmap rendering when connecting to servers that
send uncompressed bitmaps with non-aligned row widths, such as XRDP
at 16 bpp.
- Skip bitmap updates that exceed bounds ([#1146](https://github.com/Devolutions/IronRDP/issues/1146)) ([2b97a95e6d](https://github.com/Devolutions/IronRDP/commit/2b97a95e6da8833e8a84e9f42960da91eee87cd6))
After a desktop resize, an RDP server can send a burst of bitmap updates
for the old resolution before its rendering pipeline has fully
transitioned to the new one. These updates reference coordinates beyond
the current image buffer in `DecodedImage`, causing index-out-of-bounds
panics in the `apply_*` methods. On the server side, the same stale
bitmaps can reach the encoder with dimensions exceeding the negotiated
desktop size, panicking in `NoneHandler::handle()`.
This commit adds bounds checks at two levels:
- `DecodedImage::rect_fits()` guard at the entry of each `apply_*`
method, returning an empty rectangle when the update doesn't fit
- Encoder-level guard in `EncoderIter::next()` that drops
`BitmapUpdate`s exceeding the current desktop size
- Propagate negotiated share_id to all outgoing ShareDataPdu ([#1147](https://github.com/Devolutions/IronRDP/issues/1147)) ([2b24e9664d](https://github.com/Devolutions/IronRDP/commit/2b24e9664dd05620ff63a24d092377477fdde863))
## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.7.0...ironrdp-session-v0.8.0)] - 2025-12-18
## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.5.0...ironrdp-session-v0.6.0)] - 2025-08-29
### <!-- 1 -->Features
- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59))
The Quite OK Image format ([1]) losslessly compresses images to a similar size
of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding.
- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb))
Add a new QOIZ codec for SetSurface command. The PDU data contains the same
data as the QOI codec, with zstd compression.
## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.4.0...ironrdp-session-v0.4.1)] - 2025-06-27
### <!-- 1 -->Features
- More functions on `ActiveStage` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602))
- `get_dvc_by_channel_id`
- `encode_dvc_messages`
## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.3.0...ironrdp-session-v0.4.0)] - 2025-05-27
### <!-- 1 -->Features
- [**breaking**] Make DecodedImage Send ([45f66117ba](https://github.com/Devolutions/IronRDP/commit/45f66117ba05170d95b21ec7d97017b44b954f28))
- Add DecodeImage helpers ([cd7a60ba45](https://github.com/Devolutions/IronRDP/commit/cd7a60ba45a0241be4ecf3860ec4f82b431a7ce2))
### <!-- 4 -->Bug Fixes
- Update rectangle when applying None codecs updates (#728) ([a50cd643dc](https://github.com/Devolutions/IronRDP/commit/a50cd643dce9621f314231b7598d2fd31e4718c6))
- Return the correct updated region ([7507a152f1](https://github.com/Devolutions/IronRDP/commit/7507a152f14db594e4067bbc01e243cfba77770f))
"update_rectangle" is set to empty(). The surface updates are then added
by "union". But a union with an empty rectangle at (0,0) is still a
rectangle at (0,0). We end up with big region updates rooted at (0,0)...
- Decrease verbosity of Rfx frame_index ([b31b99eafb](https://github.com/Devolutions/IronRDP/commit/b31b99eafb0aac2a5e5a610af21a4027ae5cd698))
- Decrease verbosity of FastPath header ([f9b6992e74](https://github.com/Devolutions/IronRDP/commit/f9b6992e74abb929f3001e76abaff5d7215e1cb4))
## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.3...ironrdp-session-v0.3.0)] - 2025-03-12
### <!-- 7 -->Build
- Bump ironrdp-pdu
## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.2...ironrdp-session-v0.2.3)] - 2025-03-12
### <!-- 7 -->Build
- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa))
## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.1...ironrdp-session-v0.2.2)] - 2025-01-28
### <!-- 6 -->Documentation
- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b))
## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.0...ironrdp-session-v0.2.1)] - 2024-12-14
### Other
- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a))
+735
View File
@@ -0,0 +1,735 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "asn1-rs"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
dependencies = [
"asn1-rs-derive",
"asn1-rs-impl",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
"thiserror",
]
[[package]]
name = "asn1-rs-derive"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "asn1-rs-impl"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bitvec"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"der_derive",
"flagset",
"zeroize",
]
[[package]]
name = "der-parser"
version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
dependencies = [
"asn1-rs",
"displaydoc",
"nom",
"num-traits",
"rusticata-macros",
]
[[package]]
name = "der_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flagset"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "ironrdp-bulk"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e548d9fd162558a5a8aaed72e528556ce88e77773634e3722b8d01d6f170388"
[[package]]
name = "ironrdp-core"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0875b98275068b88652e49ba2e0a1150a1390aad69d26c942c8c59e8ec918e"
dependencies = [
"ironrdp-error",
]
[[package]]
name = "ironrdp-displaycontrol"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74a111a6fd25abbe51b6a15276fe980c6b9eee50a1421224b0c3f45848d45bcf"
dependencies = [
"ironrdp-core",
"ironrdp-dvc",
"ironrdp-pdu",
"ironrdp-svc",
"tracing",
]
[[package]]
name = "ironrdp-dvc"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a5de64988ddabf96928e2f042e1772a5f7201f0001ea99012129c73a105fc52"
dependencies = [
"ironrdp-core",
"ironrdp-pdu",
"ironrdp-svc",
"tracing",
]
[[package]]
name = "ironrdp-error"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd344ce9518ab83f6f7568ca4f1bc6dc8c55bd2da04cb5ee7b3e8740d5041734"
[[package]]
name = "ironrdp-graphics"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7493e426b6a8104cd497e518ba7781a9c7fc9a5f58a9cc0c1111e6d66f63bcc"
dependencies = [
"bit_field",
"bitflags",
"bitvec",
"byteorder",
"ironrdp-core",
"ironrdp-pdu",
"num-derive",
"num-traits",
"yuv",
]
[[package]]
name = "ironrdp-pdu"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ccd1179a4d106df1930347701388b5c79bc3725fa7dab4438d57db0d9d19347"
dependencies = [
"bit_field",
"bitflags",
"byteorder",
"der-parser",
"ironrdp-core",
"ironrdp-error",
"md-5",
"num-bigint",
"num-derive",
"num-integer",
"num-traits",
"pkcs1",
"sha1",
"tap",
"x509-cert",
]
[[package]]
name = "ironrdp-session"
version = "0.11.0"
dependencies = [
"ironrdp-bulk",
"ironrdp-core",
"ironrdp-displaycontrol",
"ironrdp-dvc",
"ironrdp-error",
"ironrdp-graphics",
"ironrdp-pdu",
"ironrdp-svc",
"qoicoubeh",
"tracing",
"zstd-safe",
]
[[package]]
name = "ironrdp-svc"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24c36b82ab0f7fef2668fb7004008a0f3c100a3d9a7b18bd4495a71aba55b796"
dependencies = [
"bitflags",
"ironrdp-core",
"ironrdp-pdu",
]
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "qoicoubeh"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba"
dependencies = [
"bytemuck",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rusticata-macros"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
dependencies = [
"nom",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tls_codec"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
dependencies = [
"tls_codec_derive",
"zeroize",
]
[[package]]
name = "tls_codec_derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x509-cert"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
dependencies = [
"const-oid",
"der",
"spki",
"tls_codec",
]
[[package]]
name = "yuv"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d85a782d94ee43f078bcfd6fa82d4e6a5b2d1cfbbad168e4df5a9f7b39ef48c"
dependencies = [
"num-traits",
]
[[package]]
name = "zeroize"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",
]
+210
View File
@@ -0,0 +1,210 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.89"
name = "ironrdp-session"
version = "0.11.0"
authors = [
"Devolutions Inc. <infos@devolutions.net>",
"Teleport <goteleport.com>",
]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "State machines to drive an RDP session"
homepage = "https://github.com/Devolutions/IronRDP"
readme = "README.md"
keywords = [
"rdp",
"remote-desktop",
"network",
"client",
"protocol",
]
categories = ["network-programming"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/Devolutions/IronRDP"
resolver = "2"
[features]
default = []
qoi = [
"dep:qoicoubeh",
"ironrdp-pdu/qoi",
]
qoiz = [
"dep:zstd-safe",
"qoi",
]
[lib]
name = "ironrdp_session"
path = "src/lib.rs"
test = false
doctest = false
[dependencies.ironrdp-bulk]
version = "0.1"
[dependencies.ironrdp-core]
version = "0.2"
[dependencies.ironrdp-displaycontrol]
version = "0.8"
[dependencies.ironrdp-dvc]
version = "0.8"
[dependencies.ironrdp-error]
version = "0.2"
[dependencies.ironrdp-graphics]
version = "0.9"
[dependencies.ironrdp-pdu]
version = "0.9"
features = ["std"]
[dependencies.ironrdp-svc]
version = "0.8"
[dependencies.qoicoubeh]
version = "0.5"
optional = true
[dependencies.tracing]
version = "0.1"
features = ["log"]
[dependencies.zstd-safe]
version = "7.2"
features = ["std"]
optional = true
[lints.clippy]
alloc_instead_of_core = "warn"
allow_attributes = "warn"
as_conversions = "warn"
as_pointer_underscore = "warn"
as_ptr_cast_mut = "warn"
as_underscore = "warn"
cast_lossless = "warn"
cast_possible_truncation = "warn"
cast_possible_wrap = "warn"
cast_ptr_alignment = "warn"
cast_sign_loss = "warn"
cfg_not_test = "warn"
checked_conversions = "warn"
clone_on_ref_ptr = "warn"
cloned_instead_of_copied = "warn"
collection_is_never_read = "warn"
copy_iterator = "warn"
dbg_macro = "warn"
deref_by_slicing = "warn"
disallowed_script_idents = "warn"
doc_include_without_cfg = "warn"
empty_drop = "warn"
empty_enum_variants_with_brackets = "warn"
expl_impl_clone_on_copy = "warn"
filetype_is_file = "warn"
float_cmp = "warn"
float_cmp_const = "warn"
fn_to_numeric_cast_any = "warn"
get_unwrap = "warn"
implicit_clone = "warn"
infinite_loop = "warn"
inline_always = "warn"
large_futures = "warn"
large_include_file = "warn"
large_stack_frames = "warn"
large_types_passed_by_value = "warn"
lossy_float_literal = "warn"
map_with_unused_argument_over_ranges = "warn"
mem_forget = "warn"
missing_panics_doc = "warn"
missing_safety_doc = "warn"
mixed_read_write_in_expression = "warn"
multiple_inherent_impl = "warn"
multiple_unsafe_ops_per_block = "warn"
needless_raw_strings = "warn"
non_ascii_literal = "warn"
non_zero_suggestions = "warn"
or_fun_call = "warn"
panic = "warn"
partial_pub_fields = "warn"
precedence_bits = "warn"
print_stderr = "warn"
print_stdout = "warn"
ptr_cast_constness = "warn"
pub_without_shorthand = "warn"
range_plus_one = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
redundant_clone = "warn"
redundant_type_annotations = "warn"
renamed_function_params = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_self_not_must_use = "warn"
same_name_method = "warn"
self_named_module_files = "warn"
semicolon_outside_block = "warn"
separated_literal_suffix = "warn"
similar_names = "warn"
std_instead_of_core = "warn"
str_to_string = "warn"
string_add = "warn"
string_lit_chars_any = "warn"
string_slice = "warn"
suspicious_xor_used_as_pow = "warn"
todo = "warn"
trait_duplication_in_bounds = "warn"
transmute_ptr_to_ptr = "warn"
try_err = "warn"
type_repetition_in_bounds = "warn"
undocumented_unsafe_blocks = "warn"
unnecessary_box_returns = "warn"
unnecessary_safety_comment = "warn"
unnecessary_self_imports = "warn"
unused_result_ok = "warn"
unused_self = "warn"
unused_trait_names = "warn"
unwrap_used = "warn"
useless_let_if_seq = "warn"
wildcard_dependencies = "warn"
wildcard_imports = "warn"
[lints.rust]
absolute_paths_not_starting_with_crate = "warn"
ambiguous_negative_literals = "warn"
elided_lifetimes_in_paths = "warn"
invalid_reference_casting = "warn"
keyword_idents = "warn"
macro_use_extern_crate = "warn"
missing_unsafe_on_extern = "warn"
noop_method_call = "warn"
redundant_imports = "warn"
redundant_lifetimes = "warn"
single_use_lifetimes = "warn"
trivial_numeric_casts = "warn"
unit_bindings = "warn"
unreachable_pub = "warn"
unsafe_attr_outside_unsafe = "warn"
unsafe_op_in_unsafe_fn = "warn"
unused_crate_dependencies = "warn"
unused_lifetimes = "warn"
unused_macro_rules = "warn"
unused_qualifications = "warn"
unused_unsafe = "warn"
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "ironrdp-session"
version = "0.11.0"
readme = "README.md"
description = "State machines to drive an RDP session"
edition.workspace = true
rust-version = "1.89"
license.workspace = true
homepage.workspace = true
repository.workspace = true
authors.workspace = true
keywords.workspace = true
categories.workspace = true
[lib]
doctest = false
test = false
[features]
default = []
qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"]
qoiz = ["dep:zstd-safe", "qoi"]
[dependencies]
ironrdp-bulk = { path = "../ironrdp-bulk", version = "0.1" }
ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public
ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public
ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public
ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public
ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public
ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public
ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" }
tracing = { version = "0.1", features = ["log"] }
qoicoubeh = { version = "0.5", optional = true }
zstd-safe = { version = "7.2", optional = true, features = ["std"] }
[lints]
workspace = true
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+23
View File
@@ -0,0 +1,23 @@
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
# IronRDP Session
Abstract state machine to drive an RDP session.
This crate is part of the [IronRDP] project.
[IronRDP]: https://github.com/Devolutions/IronRDP
+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)
}