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:
+214
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,469 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_bulk::BulkCompressor;
|
||||
use ironrdp_core::{ReadCursor, WriteBuf};
|
||||
use ironrdp_displaycontrol::client::DisplayControlClient;
|
||||
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
|
||||
use ironrdp_graphics::pointer::DecodedPointer;
|
||||
use ironrdp_pdu::geometry::InclusiveRectangle;
|
||||
use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent};
|
||||
use ironrdp_pdu::rdp::autodetect::AutoDetectRequest;
|
||||
use ironrdp_pdu::rdp::client_info::CompressionType as PduCompressionType;
|
||||
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
|
||||
use ironrdp_pdu::slow_path::{self, GraphicsUpdateType};
|
||||
use ironrdp_pdu::{Action, mcs};
|
||||
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::fast_path::UpdateKind;
|
||||
use crate::image::DecodedImage;
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, fast_path, x224};
|
||||
|
||||
/// Converts the PDU-layer compression type to the bulk crate's compression type.
|
||||
fn to_bulk_compression_type(ct: PduCompressionType) -> ironrdp_bulk::CompressionType {
|
||||
match ct {
|
||||
PduCompressionType::K8 => ironrdp_bulk::CompressionType::Rdp4,
|
||||
PduCompressionType::K64 => ironrdp_bulk::CompressionType::Rdp5,
|
||||
PduCompressionType::Rdp6 => ironrdp_bulk::CompressionType::Rdp6,
|
||||
PduCompressionType::Rdp61 => ironrdp_bulk::CompressionType::Rdp61,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ActiveStage {
|
||||
x224_processor: x224::Processor,
|
||||
fast_path_processor: fast_path::Processor,
|
||||
enable_server_pointer: bool,
|
||||
}
|
||||
|
||||
/// Builder for [`ActiveStage`].
|
||||
///
|
||||
/// All fields are required; they are typically taken straight from `ironrdp-connector`’s
|
||||
/// `ConnectionResult` once the connection sequence is finalized.
|
||||
pub struct ActiveStageBuilder {
|
||||
pub static_channels: StaticChannelSet,
|
||||
pub user_channel_id: u16,
|
||||
pub io_channel_id: u16,
|
||||
pub message_channel_id: Option<u16>,
|
||||
pub share_id: u32,
|
||||
/// The bulk compression type that was negotiated, if any.
|
||||
pub compression_type: Option<PduCompressionType>,
|
||||
/// Enable server-side pointer updates (client-side pointer rendering).
|
||||
pub enable_server_pointer: bool,
|
||||
/// Use software rendering mode for pointer bitmap generation.
|
||||
pub pointer_software_rendering: bool,
|
||||
}
|
||||
|
||||
impl ActiveStageBuilder {
|
||||
pub fn build(self) -> ActiveStage {
|
||||
let Self {
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
compression_type,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
} = self;
|
||||
|
||||
let x224_processor = x224::Processor::new(
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
);
|
||||
|
||||
// Create bulk decompressor if compression was negotiated
|
||||
let bulk_decompressor = compression_type.and_then(|ct| {
|
||||
let bulk_ct = to_bulk_compression_type(ct);
|
||||
match BulkCompressor::new(bulk_ct) {
|
||||
Ok(compressor) => {
|
||||
info!(compression_type = %bulk_ct, "Bulk decompressor initialized for FastPath");
|
||||
Some(compressor)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to create bulk decompressor, compression disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let fast_path_processor = fast_path::ProcessorBuilder {
|
||||
io_channel_id,
|
||||
user_channel_id,
|
||||
share_id,
|
||||
enable_server_pointer,
|
||||
pointer_software_rendering,
|
||||
bulk_decompressor,
|
||||
}
|
||||
.build();
|
||||
|
||||
ActiveStage {
|
||||
x224_processor,
|
||||
fast_path_processor,
|
||||
enable_server_pointer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveStage {
|
||||
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
|
||||
self.fast_path_processor.update_mouse_pos(x, y);
|
||||
}
|
||||
|
||||
/// Encodes outgoing input events and modifies image if necessary (e.g for client-side pointer
|
||||
/// rendering).
|
||||
pub fn process_fastpath_input(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
events: &[FastPathInputEvent],
|
||||
) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
if events.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Mouse move events are prevalent, so we can preallocate space for
|
||||
// response frame + graphics update
|
||||
let mut output = Vec::with_capacity(2);
|
||||
|
||||
// Encoding fastpath response frame
|
||||
// PERF: unnecessary copy
|
||||
let fastpath_input = FastPathInput::new(events.to_vec()).map_err(SessionError::decode)?;
|
||||
let frame = ironrdp_core::encode_vec(&fastpath_input).map_err(SessionError::encode)?;
|
||||
output.push(ActiveStageOutput::ResponseFrame(frame));
|
||||
|
||||
// If pointer rendering is disabled - we can skip the rest
|
||||
if !self.enable_server_pointer {
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
// If mouse was moved by client - we should update framebuffer to reflect new
|
||||
// pointer position
|
||||
let mouse_pos = events.iter().find_map(|event| match event {
|
||||
FastPathInputEvent::MouseEvent(event) => Some((event.x_position, event.y_position)),
|
||||
FastPathInputEvent::MouseEventEx(event) => Some((event.x_position, event.y_position)),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
let (mouse_x, mouse_y) = match mouse_pos {
|
||||
Some(mouse_pos) => mouse_pos,
|
||||
None => return Ok(output),
|
||||
};
|
||||
|
||||
// Graphics update is only sent when update is visually changed the framebuffer
|
||||
if let Some(rect) = image.move_pointer(mouse_x, mouse_y)? {
|
||||
output.push(ActiveStageOutput::GraphicsUpdate(rect));
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Process a frame received from the server.
|
||||
pub fn process(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
action: Action,
|
||||
frame: &[u8],
|
||||
) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
let (mut stage_outputs, processor_updates) = match action {
|
||||
Action::FastPath => {
|
||||
let mut output = WriteBuf::new();
|
||||
let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?;
|
||||
(
|
||||
vec![ActiveStageOutput::ResponseFrame(output.into_inner())],
|
||||
processor_updates,
|
||||
)
|
||||
}
|
||||
Action::X224 => {
|
||||
let x224_outputs = self.x224_processor.process(frame)?;
|
||||
let mut stage_outputs = Vec::new();
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
for output in x224_outputs {
|
||||
match output {
|
||||
x224::ProcessorOutput::GraphicsUpdate(data) => {
|
||||
let updates = process_slow_path_graphics(&mut self.fast_path_processor, image, &data)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
x224::ProcessorOutput::PointerUpdate(data) => {
|
||||
let updates = process_slow_path_pointer(&mut self.fast_path_processor, image, &data)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
other => {
|
||||
stage_outputs.push(ActiveStageOutput::try_from(other)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(stage_outputs, processor_updates)
|
||||
}
|
||||
};
|
||||
|
||||
for update in processor_updates {
|
||||
match update {
|
||||
UpdateKind::None => {}
|
||||
UpdateKind::Region(region) => {
|
||||
stage_outputs.push(ActiveStageOutput::GraphicsUpdate(region));
|
||||
}
|
||||
UpdateKind::PointerDefault => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerDefault);
|
||||
}
|
||||
UpdateKind::PointerHidden => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerHidden);
|
||||
}
|
||||
UpdateKind::PointerPosition { x, y } => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerPosition { x, y });
|
||||
}
|
||||
UpdateKind::PointerBitmap(pointer) => {
|
||||
stage_outputs.push(ActiveStageOutput::PointerBitmap(pointer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stage_outputs)
|
||||
}
|
||||
|
||||
pub fn set_fastpath_processor(&mut self, processor: fast_path::Processor) {
|
||||
self.fast_path_processor = processor;
|
||||
}
|
||||
|
||||
/// Updates the share_id used by the x224 processor for encoding ShareDataPdu.
|
||||
/// Must be called during Deactivation-Reactivation if the server assigns a new share_id.
|
||||
pub fn set_share_id(&mut self, share_id: u32) {
|
||||
self.x224_processor.set_share_id(share_id);
|
||||
}
|
||||
|
||||
pub fn set_enable_server_pointer(&mut self, enable_server_pointer: bool) {
|
||||
self.enable_server_pointer = enable_server_pointer;
|
||||
}
|
||||
|
||||
/// Encodes client-side graceful shutdown request. Note that upon sending this request,
|
||||
/// client should wait for server's ShutdownDenied PDU before closing the connection.
|
||||
///
|
||||
/// Client-side graceful shutdown is defined in [MS-RDPBCGR]
|
||||
///
|
||||
/// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||
pub fn graceful_shutdown(&self) -> SessionResult<Vec<ActiveStageOutput>> {
|
||||
let mut frame = WriteBuf::new();
|
||||
self.x224_processor
|
||||
.encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?;
|
||||
|
||||
Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())])
|
||||
}
|
||||
|
||||
/// Send a pdu on the static global channel. Typically used to send input events
|
||||
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
|
||||
self.x224_processor.encode_static(output, pdu)
|
||||
}
|
||||
|
||||
pub fn get_svc_processor<T: SvcProcessor + 'static>(&mut self) -> Option<&T> {
|
||||
self.x224_processor.get_svc_processor()
|
||||
}
|
||||
|
||||
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
|
||||
self.x224_processor.get_svc_processor_mut()
|
||||
}
|
||||
|
||||
pub fn get_dvc<T: DvcProcessor + 'static>(&mut self) -> Option<&DynamicVirtualChannel> {
|
||||
self.x224_processor.get_dvc::<T>()
|
||||
}
|
||||
|
||||
pub fn get_dvc_by_channel_id(&mut self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
|
||||
self.x224_processor.get_dvc_by_channel_id(channel_id)
|
||||
}
|
||||
|
||||
/// Completes user's SVC request with data, required to sent it over the network and returns
|
||||
/// a buffer with encoded data.
|
||||
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
|
||||
&self,
|
||||
messages: SvcProcessorMessages<C>,
|
||||
) -> SessionResult<Vec<u8>> {
|
||||
self.x224_processor.process_svc_processor_messages(messages)
|
||||
}
|
||||
|
||||
/// Fully encodes a resize request for sending over the Display Control Virtual Channel.
|
||||
///
|
||||
/// If the Display Control Virtual Channel is not available, or not yet connected, this method
|
||||
/// will return `None`.
|
||||
///
|
||||
/// Per [2.2.2.2.1]:
|
||||
/// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
|
||||
/// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
|
||||
/// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent.
|
||||
/// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm.
|
||||
///
|
||||
/// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function
|
||||
/// to ensure the display size is within the valid range.
|
||||
///
|
||||
/// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
|
||||
pub fn encode_resize(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_dims: Option<(u32, u32)>,
|
||||
) -> Option<SessionResult<Vec<u8>>> {
|
||||
if let Some(dvc) = self.get_dvc::<DisplayControlClient>() {
|
||||
if let Some(channel_id) = dvc.channel_id() {
|
||||
let display_control = dvc.channel_processor_downcast_ref::<DisplayControlClient>()?;
|
||||
let svc_messages = match display_control.encode_single_primary_monitor(
|
||||
channel_id,
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
physical_dims,
|
||||
) {
|
||||
Ok(messages) => messages,
|
||||
Err(e) => return Some(Err(SessionError::encode(e))),
|
||||
};
|
||||
|
||||
return Some(
|
||||
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(svc_messages)),
|
||||
);
|
||||
} else {
|
||||
debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected");
|
||||
}
|
||||
} else {
|
||||
debug!("Could not encode a resize: Display Control Virtual Channel is not available");
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn encode_dvc_messages(&mut self, messages: Vec<SvcMessage>) -> SessionResult<Vec<u8>> {
|
||||
self.process_svc_processor_messages(SvcProcessorMessages::<DrdynvcClient>::new(messages))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ActiveStageOutput {
|
||||
ResponseFrame(Vec<u8>),
|
||||
GraphicsUpdate(InclusiveRectangle),
|
||||
PointerDefault,
|
||||
PointerHidden,
|
||||
PointerPosition {
|
||||
x: u16,
|
||||
y: u16,
|
||||
},
|
||||
PointerBitmap(Arc<DecodedPointer>),
|
||||
Terminate(GracefulDisconnectReason),
|
||||
/// Received a Server Deactivate All PDU. The consumer should execute the [Deactivation-Reactivation Sequence].
|
||||
///
|
||||
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
|
||||
DeactivateAll,
|
||||
/// Server Initiate Multitransport Request. The application should establish a
|
||||
/// sideband UDP transport using the provided request parameters.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.15.1].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
|
||||
MultitransportRequest(MultitransportRequestPdu),
|
||||
/// Server-reported network characteristics ([\[MS-RDPBCGR\] 2.2.14.1.5]).
|
||||
///
|
||||
/// Contains an [`AutoDetectRequest::NetworkCharacteristicsResult`] with
|
||||
/// RTT and/or bandwidth measurements computed by the server.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.14.1.5].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.14.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/228ffc5c-b60c-4d3e-9781-ac613f822fdf
|
||||
AutoDetect(AutoDetectRequest),
|
||||
}
|
||||
|
||||
impl TryFrom<x224::ProcessorOutput> for ActiveStageOutput {
|
||||
type Error = SessionError;
|
||||
|
||||
fn try_from(value: x224::ProcessorOutput) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)),
|
||||
x224::ProcessorOutput::Disconnect(desc) => {
|
||||
let desc = match desc {
|
||||
x224::DisconnectDescription::McsDisconnect(reason) => match reason {
|
||||
mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated,
|
||||
mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated,
|
||||
other => GracefulDisconnectReason::Other(other.description().to_owned()),
|
||||
},
|
||||
x224::DisconnectDescription::ErrorInfo(info) => GracefulDisconnectReason::Other(info.description()),
|
||||
};
|
||||
|
||||
Ok(Self::Terminate(desc))
|
||||
}
|
||||
x224::ProcessorOutput::DeactivateAll => Ok(Self::DeactivateAll),
|
||||
x224::ProcessorOutput::MultitransportRequest(pdu) => Ok(Self::MultitransportRequest(pdu)),
|
||||
x224::ProcessorOutput::AutoDetect(request) => Ok(Self::AutoDetect(request)),
|
||||
// GraphicsUpdate and PointerUpdate are consumed in ActiveStage::process()
|
||||
// before reaching this conversion.
|
||||
x224::ProcessorOutput::GraphicsUpdate(_) | x224::ProcessorOutput::PointerUpdate(_) => Err(
|
||||
SessionError::general("slow-path graphics/pointer updates should be handled before this conversion"),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for
|
||||
/// disconnect reasons.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GracefulDisconnectReason {
|
||||
UserInitiated,
|
||||
ServerInitiated,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl GracefulDisconnectReason {
|
||||
pub fn description(&self) -> String {
|
||||
match self {
|
||||
GracefulDisconnectReason::UserInitiated => "user initiated disconnect".to_owned(),
|
||||
GracefulDisconnectReason::ServerInitiated => "server initiated disconnect".to_owned(),
|
||||
GracefulDisconnectReason::Other(description) => description.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for GracefulDisconnectReason {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_str(&self.description())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and process a slow-path graphics update through the shared bitmap pipeline.
|
||||
fn process_slow_path_graphics(
|
||||
fast_path_processor: &mut fast_path::Processor,
|
||||
image: &mut DecodedImage,
|
||||
data: &[u8],
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut src = ReadCursor::new(data);
|
||||
let update_type = slow_path::read_graphics_update_type(&mut src).map_err(SessionError::decode)?;
|
||||
|
||||
match update_type {
|
||||
GraphicsUpdateType::Bitmap => {
|
||||
let bitmap = slow_path::decode_slow_path_bitmap(&mut src).map_err(SessionError::decode)?;
|
||||
fast_path_processor.process_bitmap_update(image, bitmap)
|
||||
}
|
||||
GraphicsUpdateType::Orders => {
|
||||
warn!("Slow-path drawing orders not supported (MS-RDPEGDI)");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
GraphicsUpdateType::Palette => {
|
||||
warn!("Slow-path palette update not supported (8bpp)");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
// Synchronize is an artifact from the T.128 multipoint protocol
|
||||
// and carries no data. Safe to ignore.
|
||||
GraphicsUpdateType::Synchronize => {
|
||||
debug!("Ignoring slow-path synchronize update");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and process a slow-path pointer update through the shared pointer pipeline.
|
||||
fn process_slow_path_pointer(
|
||||
fast_path_processor: &mut fast_path::Processor,
|
||||
image: &mut DecodedImage,
|
||||
data: &[u8],
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut src = ReadCursor::new(data);
|
||||
let pointer = slow_path::decode_slow_path_pointer(&mut src).map_err(SessionError::decode)?;
|
||||
fast_path_processor.process_pointer_update(image, pointer)
|
||||
}
|
||||
+821
@@ -0,0 +1,821 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_bulk::BulkCompressor;
|
||||
use ironrdp_core::{DecodeErrorKind, ReadCursor, WriteBuf, decode_cursor};
|
||||
use ironrdp_graphics::image_processing::PixelFormat;
|
||||
use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget};
|
||||
use ironrdp_graphics::rdp6::BitmapStreamDecoder;
|
||||
use ironrdp_graphics::rle::RlePixelFormat;
|
||||
use ironrdp_pdu::bitmap::BitmapUpdateData;
|
||||
use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu;
|
||||
use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation};
|
||||
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
|
||||
use ironrdp_pdu::pointer::PointerUpdateData;
|
||||
use ironrdp_pdu::rdp::capability_sets::{CODEC_ID_NONE, CODEC_ID_REMOTEFX, CodecId};
|
||||
use ironrdp_pdu::rdp::headers::{CompressionFlags, ShareDataPdu};
|
||||
use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::image::DecodedImage;
|
||||
use crate::palette::Palette;
|
||||
use crate::pointer::PointerCache;
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, custom_err, reason_err, rfx};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ironrdp_pdu::bitmap::{BitmapData, Compression};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn raw_bitmap_removes_byte_padding_without_changing_source_stride() {
|
||||
let mut processor = ProcessorBuilder {
|
||||
io_channel_id: 0,
|
||||
user_channel_id: 0,
|
||||
share_id: 0,
|
||||
enable_server_pointer: false,
|
||||
pointer_software_rendering: false,
|
||||
bulk_decompressor: None,
|
||||
}
|
||||
.build();
|
||||
let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3);
|
||||
|
||||
// Two bottom-up BGR rows of three source pixels each. The final source
|
||||
// column is outside the destination and each row has three pad bytes.
|
||||
let bitmap_data = [
|
||||
3, 2, 1, 6, 5, 4, 9, 8, 7, 0xff, 0xff, 0xff, // bottom row
|
||||
15, 14, 13, 18, 17, 16, 21, 20, 19, 0xff, 0xff, 0xff, // top row
|
||||
];
|
||||
let bitmap_update = BitmapUpdateData {
|
||||
rectangles: vec![BitmapData {
|
||||
rectangle: InclusiveRectangle {
|
||||
left: 1,
|
||||
top: 0,
|
||||
right: 2,
|
||||
bottom: 1,
|
||||
},
|
||||
width: 3,
|
||||
height: 2,
|
||||
bits_per_pixel: 24,
|
||||
compression_flags: Compression::empty(),
|
||||
compressed_data_header: None,
|
||||
bitmap_data: &bitmap_data,
|
||||
}],
|
||||
};
|
||||
|
||||
processor.process_bitmap_update(&mut image, bitmap_update).unwrap();
|
||||
|
||||
let pixel = |x: usize, y: usize| -> [u8; 4] {
|
||||
let offset = (y * usize::from(image.width()) + x) * 4;
|
||||
image.data()[offset..offset + 4]
|
||||
.try_into()
|
||||
.expect("pixel has four channels")
|
||||
};
|
||||
assert_eq!(pixel(1, 0), [13, 14, 15, 255]);
|
||||
assert_eq!(pixel(2, 0), [16, 17, 18, 255]);
|
||||
assert_eq!(pixel(1, 1), [1, 2, 3, 255]);
|
||||
assert_eq!(pixel(2, 1), [4, 5, 6, 255]);
|
||||
assert_eq!(pixel(3, 0), [0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UpdateKind {
|
||||
None,
|
||||
Region(InclusiveRectangle),
|
||||
PointerDefault,
|
||||
PointerHidden,
|
||||
PointerPosition { x: u16, y: u16 },
|
||||
PointerBitmap(Arc<DecodedPointer>),
|
||||
}
|
||||
|
||||
pub struct Processor {
|
||||
complete_data: CompleteData,
|
||||
rfx_handler: rfx::DecodingContext,
|
||||
marker_processor: FrameMarkerProcessor,
|
||||
bitmap_stream_decoder: BitmapStreamDecoder,
|
||||
pointer_cache: PointerCache,
|
||||
use_system_pointer: bool,
|
||||
mouse_pos_update: Option<(u16, u16)>,
|
||||
enable_server_pointer: bool,
|
||||
pointer_software_rendering: bool,
|
||||
/// Bulk decompressor for server-to-client compressed PDUs.
|
||||
/// `None` when compression was not negotiated.
|
||||
bulk_decompressor: Option<BulkCompressor>,
|
||||
/// Current 8bpp color palette. Updated by Palette fast-path updates.
|
||||
palette: Palette,
|
||||
#[cfg(feature = "qoiz")]
|
||||
zdctx: zstd_safe::DCtx<'static>,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
pub fn update_mouse_pos(&mut self, x: u16, y: u16) {
|
||||
self.mouse_pos_update = Some((x, y));
|
||||
}
|
||||
|
||||
/// Process input fast path frame and return list of updates.
|
||||
pub fn process(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
input: &[u8],
|
||||
output: &mut WriteBuf,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
if let Some((x, y)) = self.mouse_pos_update.take() {
|
||||
if let Some(rect) = image.move_pointer(x, y)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
|
||||
let mut input = ReadCursor::new(input);
|
||||
|
||||
let header = decode_cursor::<FastPathHeader>(&mut input).map_err(SessionError::decode)?;
|
||||
trace!(fast_path_header = ?header, "Received Fast-Path packet");
|
||||
|
||||
// A single FastPath output PDU can contain multiple updates.
|
||||
// Loop over all updates within the PDU payload.
|
||||
while !input.is_empty() {
|
||||
let update_result = self.process_single_update(&mut input, image, output)?;
|
||||
processor_updates.extend(update_result);
|
||||
}
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
/// Process a single FastPath update from the cursor, advancing past it.
|
||||
fn process_single_update(
|
||||
&mut self,
|
||||
input: &mut ReadCursor<'_>,
|
||||
image: &mut DecodedImage,
|
||||
output: &mut WriteBuf,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
let update_pdu = decode_cursor::<FastPathUpdatePdu<'_>>(input).map_err(SessionError::decode)?;
|
||||
trace!(fast_path_update_fragmentation = ?update_pdu.fragmentation);
|
||||
|
||||
// Decompress the payload if the server sent it compressed.
|
||||
let decompressed_data;
|
||||
let payload = if let Some(flags) = update_pdu.compression_flags {
|
||||
if flags.contains(CompressionFlags::COMPRESSED) || flags.contains(CompressionFlags::FLUSHED) {
|
||||
let bulk_flags =
|
||||
u32::from(flags.bits()) | u32::from(update_pdu.compression_type.map_or(0, |ct| ct.as_u8()));
|
||||
|
||||
if let Some(ref mut decompressor) = self.bulk_decompressor {
|
||||
let decompressed = decompressor
|
||||
.decompress(update_pdu.data, bulk_flags)
|
||||
.map_err(|e| reason_err!("FastPath", "bulk decompression failed: {}", e))?;
|
||||
// Copy decompressed data before accessing metrics (releases the mutable borrow).
|
||||
decompressed_data = decompressed.to_vec();
|
||||
debug!(
|
||||
compressed_size = update_pdu.data.len(),
|
||||
decompressed_size = decompressed_data.len(),
|
||||
compression_type = ?update_pdu.compression_type,
|
||||
compression_ratio = format_args!("{:.2}x", decompressor.compression_ratio()),
|
||||
total_compressed = decompressor.total_compressed_bytes(),
|
||||
total_uncompressed = decompressor.total_uncompressed_bytes(),
|
||||
"Decompressed FastPath update"
|
||||
);
|
||||
decompressed_data.as_slice()
|
||||
} else {
|
||||
warn!("Received compressed FastPath data but no decompressor is configured");
|
||||
update_pdu.data
|
||||
}
|
||||
} else {
|
||||
// Compression flags present but COMPRESSED bit not set — pass data through.
|
||||
// Still need to inform the decompressor of FLUSHED/AT_FRONT flags even
|
||||
// without compressed payload.
|
||||
update_pdu.data
|
||||
}
|
||||
} else {
|
||||
update_pdu.data
|
||||
};
|
||||
|
||||
let processed_complete_data = self.complete_data.process_data(payload, update_pdu.fragmentation);
|
||||
|
||||
let update_code = update_pdu.update_code;
|
||||
|
||||
let Some(data) = processed_complete_data else {
|
||||
return Ok(processor_updates);
|
||||
};
|
||||
|
||||
let update = FastPathUpdate::decode_with_code(data.as_slice(), update_code);
|
||||
|
||||
match update {
|
||||
Ok(FastPathUpdate::SurfaceCommands(surface_commands)) => {
|
||||
trace!("Received Surface Commands: {} pieces", surface_commands.len());
|
||||
let update_region = self.process_surface_commands(image, output, surface_commands)?;
|
||||
processor_updates.push(UpdateKind::Region(update_region));
|
||||
}
|
||||
Ok(FastPathUpdate::Bitmap(bitmap_update)) => {
|
||||
trace!("Received bitmap update");
|
||||
let updates = self.process_bitmap_update(image, bitmap_update)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
Ok(FastPathUpdate::Pointer(update)) => {
|
||||
let updates = self.process_pointer_update(image, update)?;
|
||||
processor_updates.extend(updates);
|
||||
}
|
||||
Ok(FastPathUpdate::Palette(palette_data)) => {
|
||||
trace!("Received palette update");
|
||||
self.palette.process_update(palette_data);
|
||||
}
|
||||
Err(e) => {
|
||||
// FIXME: This seems to be a way of special-handling the error case in FastPathUpdate::decode_cursor_with_code
|
||||
// to ignore the unsupported update PDUs, but this is a fragile logic and the rationale behind it is not
|
||||
// obvious.
|
||||
if let DecodeErrorKind::InvalidField { field, reason } = e.kind() {
|
||||
warn!(field, reason, "Received invalid Fast-Path update");
|
||||
processor_updates.push(UpdateKind::None);
|
||||
} else {
|
||||
return Err(custom_err!("Fast-Path", e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
/// Process a bitmap update, shared between fast-path and slow-path pipelines.
|
||||
pub fn process_bitmap_update(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
bitmap_update: BitmapUpdateData<'_>,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut buf = Vec::new();
|
||||
let mut update_kind = UpdateKind::None;
|
||||
|
||||
for update in bitmap_update.rectangles {
|
||||
trace!("{update:?}");
|
||||
buf.clear();
|
||||
|
||||
let width = update.width.min(update.rectangle.width());
|
||||
let height = update.height.min(update.rectangle.height());
|
||||
if width == 0 || height == 0 {
|
||||
warn!(
|
||||
bitmap_width = update.width,
|
||||
bitmap_height = update.height,
|
||||
destination = ?update.rectangle,
|
||||
"Skipping bitmap with an empty source or destination"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// `width` and `height` describe the source bitmap layout, while
|
||||
// destination bounds describe where its visible portion is drawn.
|
||||
// Servers can send source rows wider than the destination; preserve
|
||||
// that source stride and discard only the excess source extent.
|
||||
let bitmap_rectangle = InclusiveRectangle {
|
||||
left: update.rectangle.left,
|
||||
top: update.rectangle.top,
|
||||
right: update.rectangle.left + width - 1,
|
||||
bottom: update.rectangle.top + height - 1,
|
||||
};
|
||||
|
||||
// Bitmap data is either compressed or uncompressed, depending
|
||||
// on whether the BITMAP_COMPRESSION flag is present in the
|
||||
// flags field.
|
||||
let update_rectangle = if update
|
||||
.compression_flags
|
||||
.contains(ironrdp_pdu::bitmap::Compression::BITMAP_COMPRESSION)
|
||||
{
|
||||
if update.bits_per_pixel == 32 {
|
||||
// Compressed bitmaps at a color depth of 32 bpp are compressed using RDP 6.0
|
||||
// Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream
|
||||
// structure ([MS-RDPEGDI] section 2.2.2.5.1).
|
||||
debug!("32 bpp compressed RDP6_BITMAP_STREAM");
|
||||
|
||||
match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24(
|
||||
update.bitmap_data,
|
||||
&mut buf,
|
||||
usize::from(update.width),
|
||||
usize::from(update.height),
|
||||
) {
|
||||
// RDP6 bitmap streams are bottom-up (upstream #1524).
|
||||
Ok(()) => image.apply_rgb24(&buf, &bitmap_rectangle, update.width, true)?,
|
||||
Err(err) => {
|
||||
warn!("Invalid RDP6_BITMAP_STREAM: {err}");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Compressed bitmaps not in 32 bpp format are compressed using Interleaved
|
||||
// RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section
|
||||
// 2.2.9.1.1.3.1.2.4).
|
||||
debug!(bpp = update.bits_per_pixel, "Non-32 bpp compressed RLE_BITMAP_STREAM",);
|
||||
|
||||
match ironrdp_graphics::rle::decompress(
|
||||
update.bitmap_data,
|
||||
&mut buf,
|
||||
usize::from(update.width),
|
||||
usize::from(update.height),
|
||||
usize::from(update.bits_per_pixel),
|
||||
) {
|
||||
Ok(RlePixelFormat::Rgb16) => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb15) => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb24) => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
Ok(RlePixelFormat::Rgb8) => image.apply_rgb8_with_palette(
|
||||
&buf,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
|
||||
Err(e) => {
|
||||
warn!("Invalid RLE-compressed bitmap: {e}");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Uncompressed bitmap data is formatted as a bottom-up, left-to-right series of
|
||||
// pixels. Each pixel is a whole number of bytes. Each row contains a multiple of
|
||||
// four bytes (including up to three bytes of padding, as necessary).
|
||||
// [MS-RDPBCGR] 2.2.9.1.1.3.1.2.2
|
||||
trace!("Uncompressed raw bitmap");
|
||||
|
||||
let bpp = usize::from(update.bits_per_pixel);
|
||||
let width = usize::from(update.width);
|
||||
let bytes_per_pixel = bpp.div_ceil(8);
|
||||
let row_bytes = width * bytes_per_pixel;
|
||||
let padded_row_bytes = (row_bytes + 3) & !3;
|
||||
|
||||
if padded_row_bytes != row_bytes {
|
||||
// Strip only byte padding; the tightly packed rows still retain
|
||||
// `update.width` pixels and therefore their source stride.
|
||||
buf.clear();
|
||||
for row in update
|
||||
.bitmap_data
|
||||
.chunks_exact(padded_row_bytes)
|
||||
.take(usize::from(update.height))
|
||||
{
|
||||
buf.extend_from_slice(&row[..row_bytes]);
|
||||
}
|
||||
|
||||
match update.bits_per_pixel {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
&buf,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
16 => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
24 => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?,
|
||||
32 => image.apply_rgb32_bitmap(&buf, PixelFormat::BgrX32, &bitmap_rectangle, update.width)?,
|
||||
_ => {
|
||||
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match update.bits_per_pixel {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
update.bitmap_data,
|
||||
&bitmap_rectangle,
|
||||
self.palette.colors(),
|
||||
update.width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
16 => image.apply_rgb16_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
24 => image.apply_bgr24_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?,
|
||||
32 => image.apply_rgb32_bitmap(
|
||||
update.bitmap_data,
|
||||
PixelFormat::BgrX32,
|
||||
&bitmap_rectangle,
|
||||
update.width,
|
||||
)?,
|
||||
_ => {
|
||||
warn!("Unsupported uncompressed bitmap depth: {bpp} bpp");
|
||||
update.rectangle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match update_kind {
|
||||
UpdateKind::Region(current) => update_kind = UpdateKind::Region(current.union(&update_rectangle)),
|
||||
_ => update_kind = UpdateKind::Region(update_rectangle),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![update_kind])
|
||||
}
|
||||
|
||||
/// Process a pointer update, shared between fast-path and slow-path pipelines.
|
||||
pub fn process_pointer_update(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
update: PointerUpdateData<'_>,
|
||||
) -> SessionResult<Vec<UpdateKind>> {
|
||||
let mut processor_updates = Vec::new();
|
||||
|
||||
if !self.enable_server_pointer {
|
||||
return Ok(processor_updates);
|
||||
}
|
||||
|
||||
let bitmap_target = if self.pointer_software_rendering {
|
||||
PointerBitmapTarget::Software
|
||||
} else {
|
||||
PointerBitmapTarget::Accelerated
|
||||
};
|
||||
|
||||
match update {
|
||||
PointerUpdateData::SetHidden => {
|
||||
processor_updates.push(UpdateKind::PointerHidden);
|
||||
if self.pointer_software_rendering && !self.use_system_pointer {
|
||||
self.use_system_pointer = true;
|
||||
if let Some(rect) = image.hide_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerUpdateData::SetDefault => {
|
||||
processor_updates.push(UpdateKind::PointerDefault);
|
||||
if self.pointer_software_rendering && !self.use_system_pointer {
|
||||
self.use_system_pointer = true;
|
||||
if let Some(rect) = image.hide_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
}
|
||||
PointerUpdateData::SetPosition(position) => {
|
||||
if self.use_system_pointer || !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerPosition {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
});
|
||||
} else if let Some(rect) = image.move_pointer(position.x, position.y)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Color(pointer) => {
|
||||
let cache_index = pointer.cache_index;
|
||||
|
||||
let decoded_pointer = Arc::new(
|
||||
DecodedPointer::decode_color_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode color pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Cached(cached) => {
|
||||
let cache_index = cached.cache_index;
|
||||
|
||||
if let Some(cached_pointer) = self.pointer_cache.get(usize::from(cache_index)) {
|
||||
// Disable system pointer
|
||||
processor_updates.push(UpdateKind::PointerHidden);
|
||||
self.use_system_pointer = false;
|
||||
// Send graphics update
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&cached_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(cached_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
} else {
|
||||
// In case pointer was hidden previously
|
||||
if let Some(rect) = image.show_pointer()? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Cached pointer not found {}", cache_index);
|
||||
}
|
||||
}
|
||||
PointerUpdateData::New(pointer) => {
|
||||
let cache_index = pointer.color_pointer.cache_index;
|
||||
|
||||
let decoded_pointer = Arc::new(
|
||||
DecodedPointer::decode_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
PointerUpdateData::Large(pointer) => {
|
||||
let cache_index = pointer.cache_index;
|
||||
|
||||
let decoded_pointer: Arc<DecodedPointer> = Arc::new(
|
||||
DecodedPointer::decode_large_pointer_attribute(&pointer, bitmap_target)
|
||||
.map_err(|e| SessionError::custom("failed to decode large pointer attribute", e))?,
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.pointer_cache
|
||||
.insert(usize::from(cache_index), Arc::clone(&decoded_pointer));
|
||||
|
||||
if !self.pointer_software_rendering {
|
||||
processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer)));
|
||||
} else if let Some(rect) = image.update_pointer(decoded_pointer)? {
|
||||
processor_updates.push(UpdateKind::Region(rect));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(processor_updates)
|
||||
}
|
||||
|
||||
fn process_surface_commands(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
output: &mut WriteBuf,
|
||||
surface_commands: Vec<SurfaceCommand<'_>>,
|
||||
) -> SessionResult<InclusiveRectangle> {
|
||||
let mut update_rectangle = None;
|
||||
|
||||
for command in surface_commands {
|
||||
match command {
|
||||
SurfaceCommand::SetSurfaceBits(bits) | SurfaceCommand::StreamSurfaceBits(bits) => {
|
||||
let codec_id = CodecId::from_u8(bits.extended_bitmap_data.codec_id).ok_or_else(|| {
|
||||
reason_err!(
|
||||
"Fast-Path",
|
||||
"unexpected codec ID: {:x}",
|
||||
bits.extended_bitmap_data.codec_id
|
||||
)
|
||||
})?;
|
||||
|
||||
trace!(?codec_id, "Surface bits");
|
||||
|
||||
let destination = bits.destination;
|
||||
// TODO(@pacmancoder): Correct rectangle conversion logic should
|
||||
// be revisited when `rectangle_processing.rs` from
|
||||
// `ironrdp-graphics` will be refactored to use generic `Rectangle`
|
||||
// trait instead of hardcoded `InclusiveRectangle`.
|
||||
let destination = InclusiveRectangle {
|
||||
left: destination.left,
|
||||
top: destination.top,
|
||||
right: destination.right - 1,
|
||||
bottom: destination.bottom - 1,
|
||||
};
|
||||
match codec_id {
|
||||
CODEC_ID_NONE => {
|
||||
let ext_data = bits.extended_bitmap_data;
|
||||
let source_width = destination.width();
|
||||
let rectangle = match ext_data.bpp {
|
||||
8 => image.apply_rgb8_with_palette(
|
||||
ext_data.data,
|
||||
&destination,
|
||||
self.palette.colors(),
|
||||
source_width,
|
||||
)?,
|
||||
15 => image.apply_rgb15_bitmap(ext_data.data, &destination, source_width)?,
|
||||
16 => image.apply_rgb16_bitmap(ext_data.data, &destination, source_width)?,
|
||||
24 => image.apply_bgr24_bitmap(ext_data.data, &destination, source_width)?,
|
||||
32 => image.apply_rgb32_bitmap(
|
||||
ext_data.data,
|
||||
PixelFormat::BgrX32,
|
||||
&destination,
|
||||
source_width,
|
||||
)?,
|
||||
bpp => {
|
||||
warn!("Unsupported surface CODEC_ID_NONE bpp: {bpp}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
update_rectangle = update_rectangle
|
||||
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
}
|
||||
CODEC_ID_REMOTEFX => {
|
||||
let mut data = ReadCursor::new(bits.extended_bitmap_data.data);
|
||||
while !data.is_empty() {
|
||||
let (_frame_id, rectangle) = self.rfx_handler.decode(image, &destination, &mut data)?;
|
||||
update_rectangle = update_rectangle
|
||||
.map(|rect: InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "qoi")]
|
||||
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => {
|
||||
qoi_apply(
|
||||
image,
|
||||
destination,
|
||||
bits.extended_bitmap_data.data,
|
||||
&mut update_rectangle,
|
||||
)?;
|
||||
}
|
||||
#[cfg(feature = "qoiz")]
|
||||
ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => {
|
||||
let compressed = &bits.extended_bitmap_data.data;
|
||||
let mut input = zstd_safe::InBuffer::around(compressed);
|
||||
let mut data = vec![0; compressed.len() * 4];
|
||||
let mut pos = 0;
|
||||
loop {
|
||||
let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos);
|
||||
self.zdctx
|
||||
.decompress_stream(&mut output, &mut input)
|
||||
.map_err(zstd_safe::get_error_name)
|
||||
.map_err(|e| reason_err!("zstd", "{}", e))?;
|
||||
pos = output.pos();
|
||||
if pos == output.capacity() {
|
||||
data.resize(data.capacity() * 2, 0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
qoi_apply(image, destination, &data, &mut update_rectangle)?;
|
||||
}
|
||||
_ => {
|
||||
warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
SurfaceCommand::FrameMarker(marker) => {
|
||||
trace!(
|
||||
"Frame marker: action {:?} with ID #{}",
|
||||
marker.frame_action,
|
||||
marker.frame_id.unwrap_or(0)
|
||||
);
|
||||
self.marker_processor.process(&marker, output)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(update_rectangle.unwrap_or_else(InclusiveRectangle::empty))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "qoi")]
|
||||
fn qoi_apply(
|
||||
image: &mut DecodedImage,
|
||||
destination: InclusiveRectangle,
|
||||
data: &[u8],
|
||||
update_rectangle: &mut Option<InclusiveRectangle>,
|
||||
) -> SessionResult<()> {
|
||||
let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?;
|
||||
|
||||
// Guard against a decoded buffer that doesn't match the destination
|
||||
// rectangle. `apply_rgb24`/`apply_rgba32` derive the row count from the
|
||||
// decoded length, and the only bounds check downstream (`rect_fits`)
|
||||
// validates the rectangle against the image, not the buffer against the
|
||||
// rectangle. A malformed/oversized QOI payload would otherwise drive the
|
||||
// per-row index past `self.data` and panic (client-side DoS).
|
||||
let channels = match header.channels {
|
||||
qoi::Channels::Rgb => 3,
|
||||
qoi::Channels::Rgba => 4,
|
||||
};
|
||||
let expected = usize::from(destination.width()) * usize::from(destination.height()) * channels;
|
||||
if decoded.len() != expected {
|
||||
return Err(reason_err!(
|
||||
"QOI decode",
|
||||
"decoded {} bytes, expected {} for {}x{} ({} channels)",
|
||||
decoded.len(),
|
||||
expected,
|
||||
destination.width(),
|
||||
destination.height(),
|
||||
channels
|
||||
));
|
||||
}
|
||||
|
||||
let rectangle = match header.channels {
|
||||
qoi::Channels::Rgb => image.apply_rgb24(&decoded, &destination, destination.width(), false)?,
|
||||
qoi::Channels::Rgba => image.apply_rgba32(&decoded, &destination, false)?,
|
||||
};
|
||||
|
||||
*update_rectangle = update_rectangle
|
||||
.as_ref()
|
||||
.map(|rect: &InclusiveRectangle| rect.union(&rectangle))
|
||||
.or(Some(rectangle));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct ProcessorBuilder {
|
||||
pub io_channel_id: u16,
|
||||
pub user_channel_id: u16,
|
||||
pub share_id: u32,
|
||||
/// Ignore server pointer updates.
|
||||
pub enable_server_pointer: bool,
|
||||
/// Use software rendering mode for pointer bitmap generation. When this option is active,
|
||||
/// `UpdateKind::PointerBitmap` will not be generated. Remote pointer will be drawn
|
||||
/// via software rendering on top of the output image.
|
||||
pub pointer_software_rendering: bool,
|
||||
/// Bulk decompressor for server-to-client compressed PDUs.
|
||||
/// `None` when compression was not negotiated.
|
||||
pub bulk_decompressor: Option<BulkCompressor>,
|
||||
}
|
||||
|
||||
impl ProcessorBuilder {
|
||||
pub fn build(self) -> Processor {
|
||||
Processor {
|
||||
complete_data: CompleteData::new(),
|
||||
rfx_handler: rfx::DecodingContext::new(),
|
||||
marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id, self.share_id),
|
||||
bitmap_stream_decoder: BitmapStreamDecoder::default(),
|
||||
pointer_cache: PointerCache::default(),
|
||||
use_system_pointer: true,
|
||||
mouse_pos_update: None,
|
||||
enable_server_pointer: self.enable_server_pointer,
|
||||
pointer_software_rendering: self.pointer_software_rendering,
|
||||
bulk_decompressor: self.bulk_decompressor,
|
||||
palette: Palette::system_default(),
|
||||
#[cfg(feature = "qoiz")]
|
||||
zdctx: zstd_safe::DCtx::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct CompleteData {
|
||||
fragmented_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CompleteData {
|
||||
fn new() -> Self {
|
||||
Self { fragmented_data: None }
|
||||
}
|
||||
|
||||
fn process_data(&mut self, data: &[u8], fragmentation: Fragmentation) -> Option<Vec<u8>> {
|
||||
match fragmentation {
|
||||
Fragmentation::Single => {
|
||||
self.check_data_is_empty();
|
||||
|
||||
Some(data.to_vec())
|
||||
}
|
||||
Fragmentation::First => {
|
||||
self.check_data_is_empty();
|
||||
|
||||
self.fragmented_data = Some(data.to_vec());
|
||||
|
||||
None
|
||||
}
|
||||
Fragmentation::Next => {
|
||||
self.append_data(data);
|
||||
|
||||
None
|
||||
}
|
||||
Fragmentation::Last => {
|
||||
self.append_data(data);
|
||||
|
||||
self.fragmented_data.take()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_data_is_empty(&mut self) {
|
||||
if self.fragmented_data.is_some() {
|
||||
warn!("Skipping pending Fast-Path Update internal multiple elements data");
|
||||
self.fragmented_data = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn append_data(&mut self, data: &[u8]) {
|
||||
if let Some(fragmented_data) = self.fragmented_data.as_mut() {
|
||||
fragmented_data.extend_from_slice(data);
|
||||
} else {
|
||||
warn!("Got unexpected Next fragmentation PDU without prior First fragmentation PDU");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FrameMarkerProcessor {
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
share_id: u32,
|
||||
}
|
||||
|
||||
impl FrameMarkerProcessor {
|
||||
fn new(user_channel_id: u16, io_channel_id: u16, share_id: u32) -> Self {
|
||||
Self {
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
share_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&mut self, marker: &FrameMarkerPdu, output: &mut WriteBuf) -> SessionResult<()> {
|
||||
match marker.frame_action {
|
||||
FrameAction::Begin => Ok(()),
|
||||
FrameAction::End => {
|
||||
ironrdp_pdu::rdp::headers::encode_share_data(
|
||||
self.user_channel_id,
|
||||
self.io_channel_id,
|
||||
self.share_id,
|
||||
ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu {
|
||||
frame_id: marker.frame_id.unwrap_or(0),
|
||||
}),
|
||||
output,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1038
File diff suppressed because it is too large
Load Diff
+130
@@ -0,0 +1,130 @@
|
||||
#![cfg_attr(doc, doc = include_str!("../README.md"))]
|
||||
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]
|
||||
#![allow(clippy::arithmetic_side_effects)] // FIXME: remove
|
||||
|
||||
mod macros;
|
||||
|
||||
pub mod fast_path;
|
||||
pub mod image;
|
||||
pub mod pointer;
|
||||
pub mod rfx; // FIXME: maybe this module should not be in this crate
|
||||
pub mod x224;
|
||||
|
||||
mod active_stage;
|
||||
mod palette;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
pub use active_stage::{ActiveStage, ActiveStageBuilder, ActiveStageOutput, GracefulDisconnectReason};
|
||||
|
||||
pub type SessionResult<T> = Result<T, SessionError>;
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum SessionErrorKind {
|
||||
Pdu(ironrdp_pdu::PduError),
|
||||
Encode(ironrdp_core::EncodeError),
|
||||
Decode(ironrdp_core::DecodeError),
|
||||
Reason(String),
|
||||
General,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl fmt::Display for SessionErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
SessionErrorKind::Pdu(_) => write!(f, "PDU error"),
|
||||
SessionErrorKind::Encode(_) => write!(f, "encode error"),
|
||||
SessionErrorKind::Decode(_) => write!(f, "decode error"),
|
||||
SessionErrorKind::Reason(description) => write!(f, "reason: {description}"),
|
||||
SessionErrorKind::General => write!(f, "general error"),
|
||||
SessionErrorKind::Custom => write!(f, "custom error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for SessionErrorKind {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
match &self {
|
||||
SessionErrorKind::Pdu(e) => Some(e),
|
||||
SessionErrorKind::Encode(e) => Some(e),
|
||||
SessionErrorKind::Decode(e) => Some(e),
|
||||
SessionErrorKind::Reason(_) => None,
|
||||
SessionErrorKind::General => None,
|
||||
SessionErrorKind::Custom => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SessionError = ironrdp_error::Error<SessionErrorKind>;
|
||||
|
||||
pub trait SessionErrorExt {
|
||||
fn pdu(error: ironrdp_pdu::PduError) -> Self;
|
||||
fn encode(error: ironrdp_core::EncodeError) -> Self;
|
||||
fn decode(error: ironrdp_core::DecodeError) -> Self;
|
||||
fn general(context: &'static str) -> Self;
|
||||
fn reason(context: &'static str, reason: impl Into<String>) -> Self;
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static;
|
||||
}
|
||||
|
||||
impl SessionErrorExt for SessionError {
|
||||
#[track_caller]
|
||||
fn pdu(error: ironrdp_pdu::PduError) -> Self {
|
||||
Self::new("payload error", SessionErrorKind::Pdu(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn encode(error: ironrdp_core::EncodeError) -> Self {
|
||||
Self::new("encode error", SessionErrorKind::Encode(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn decode(error: ironrdp_core::DecodeError) -> Self {
|
||||
Self::new("decode error", SessionErrorKind::Decode(error))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn general(context: &'static str) -> Self {
|
||||
Self::new(context, SessionErrorKind::General)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn reason(context: &'static str, reason: impl Into<String>) -> Self {
|
||||
Self::new(context, SessionErrorKind::Reason(reason.into()))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn custom<E>(context: &'static str, e: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
Self::new(context, SessionErrorKind::Custom).with_source(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SessionResultExt {
|
||||
#[must_use]
|
||||
fn with_context(self, context: &'static str) -> Self;
|
||||
#[must_use]
|
||||
fn with_source<E>(self, source: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static;
|
||||
}
|
||||
|
||||
impl<T> SessionResultExt for SessionResult<T> {
|
||||
fn with_context(self, context: &'static str) -> Self {
|
||||
self.map_err(|mut e| {
|
||||
e.set_context(context);
|
||||
e
|
||||
})
|
||||
}
|
||||
|
||||
fn with_source<E>(self, source: E) -> Self
|
||||
where
|
||||
E: core::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
self.map_err(|e| e.with_source(source))
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/// Creates a `SessionError` with `General` kind
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::general(context)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! general_err {
|
||||
( $context:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::general($context) }};
|
||||
}
|
||||
|
||||
/// Creates a `SessionError` with `Reason` kind
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::reason(context, reason)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! reason_err {
|
||||
( $context:expr, $($arg:tt)* ) => {{
|
||||
<$crate::SessionError as $crate::SessionErrorExt>::reason($context, format!($($arg)*))
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a `SessionError` with `Custom` kind and a source error attached to it
|
||||
///
|
||||
/// Shorthand for
|
||||
/// ```ignore
|
||||
/// <ironrdp_session::SessionError as ironrdp_session::SessionErrorExt>::custom(context, source)
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! custom_err {
|
||||
( $context:expr, $source:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::custom($context, $source) }};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! eof_try {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => {
|
||||
return Ok(None);
|
||||
}
|
||||
result => result,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_ready {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Ok(Some(v)) => Ok(v),
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
};
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
use tracing::{debug, warn};
|
||||
|
||||
/// 8bpp color palette (256 RGB entries).
|
||||
///
|
||||
/// Initialized with the default Windows system palette (VGA colors)
|
||||
/// per MS-RDPBCGR 2.2.9.1.1.3.1.1. Updated by TS_UPDATE_PALETTE_DATA
|
||||
/// fast-path updates during the session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Palette {
|
||||
colors: [[u8; 3]; 256],
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
/// Create a palette initialized with the 20 static colors from the
|
||||
/// Windows default system palette. Indices 0-9 and 246-255 are the
|
||||
/// reserved static colors; the middle 236 entries (10-245) are black.
|
||||
///
|
||||
/// Reference: <https://learn.microsoft.com/en-us/windows/win32/gdi/default-palette>
|
||||
pub(crate) fn system_default() -> Self {
|
||||
let mut colors = [[0u8; 3]; 256];
|
||||
// Lower 10 static colors (indices 0-9)
|
||||
colors[0] = [0, 0, 0]; // Black
|
||||
colors[1] = [128, 0, 0]; // Dark Red
|
||||
colors[2] = [0, 128, 0]; // Dark Green
|
||||
colors[3] = [128, 128, 0]; // Dark Yellow
|
||||
colors[4] = [0, 0, 128]; // Dark Blue
|
||||
colors[5] = [128, 0, 128]; // Dark Magenta
|
||||
colors[6] = [0, 128, 128]; // Dark Cyan
|
||||
colors[7] = [192, 192, 192]; // Light Gray
|
||||
colors[8] = [192, 220, 192]; // Money Green
|
||||
colors[9] = [166, 202, 240]; // Sky Blue
|
||||
// Upper 10 static colors (indices 246-255)
|
||||
colors[246] = [255, 251, 240]; // Cream
|
||||
colors[247] = [160, 160, 164]; // Medium Gray
|
||||
colors[248] = [128, 128, 128]; // Dark Gray
|
||||
colors[249] = [255, 0, 0]; // Red
|
||||
colors[250] = [0, 255, 0]; // Green
|
||||
colors[251] = [255, 255, 0]; // Yellow
|
||||
colors[252] = [0, 0, 255]; // Blue
|
||||
colors[253] = [255, 0, 255]; // Magenta
|
||||
colors[254] = [0, 255, 255]; // Cyan
|
||||
colors[255] = [255, 255, 255]; // White
|
||||
Self { colors }
|
||||
}
|
||||
|
||||
/// Parse TS_UPDATE_PALETTE_DATA and update palette entries.
|
||||
/// Wire format: pad(2) + numberColors(u32) + N x TS_COLOR_QUAD [B, G, R, pad].
|
||||
pub(crate) fn process_update(&mut self, data: &[u8]) {
|
||||
if data.len() < 6 {
|
||||
warn!("Palette update too short: {} bytes", data.len());
|
||||
return;
|
||||
}
|
||||
|
||||
let raw_count = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
|
||||
// Palette can have at most 256 entries; clamp before any arithmetic
|
||||
// to prevent overflow on untrusted input
|
||||
let clamped = raw_count.min(256);
|
||||
let number_colors = usize::try_from(clamped).unwrap_or(256);
|
||||
let entry_data = &data[6..];
|
||||
|
||||
let Some(required_len) = number_colors.checked_mul(4) else {
|
||||
warn!("Palette entry count overflow");
|
||||
return;
|
||||
};
|
||||
|
||||
if entry_data.len() < required_len {
|
||||
warn!(
|
||||
"Palette data truncated: expected {} bytes for {} colors, got {}",
|
||||
required_len,
|
||||
number_colors,
|
||||
entry_data.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for i in 0..number_colors {
|
||||
let offset = i * 4;
|
||||
// TS_COLOR_QUAD: Blue, Green, Red, Pad
|
||||
self.colors[i] = [entry_data[offset + 2], entry_data[offset + 1], entry_data[offset]];
|
||||
}
|
||||
|
||||
debug!("Updated palette with {} colors", number_colors);
|
||||
}
|
||||
|
||||
/// Borrow the underlying color table for bitmap application.
|
||||
pub(crate) fn colors(&self) -> &[[u8; 3]; 256] {
|
||||
&self.colors
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ironrdp_graphics::pointer::DecodedPointer;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PointerCache {
|
||||
// TODO(@pacancoder) maybe use Vec<Optional<...>> instead?
|
||||
cache: HashMap<usize, Arc<DecodedPointer>>,
|
||||
}
|
||||
|
||||
impl PointerCache {
|
||||
pub fn insert(&mut self, id: usize, pointer: Arc<DecodedPointer>) -> Option<Arc<DecodedPointer>> {
|
||||
self.cache.insert(id, pointer)
|
||||
}
|
||||
|
||||
pub fn get(&self, id: usize) -> Option<Arc<DecodedPointer>> {
|
||||
self.cache.get(&id).cloned()
|
||||
}
|
||||
|
||||
pub fn is_cached(&self, id: usize) -> bool {
|
||||
self.cache.contains_key(&id)
|
||||
}
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
use core::cmp::min;
|
||||
|
||||
use ironrdp_graphics::color_conversion::{self, YCbCrBuffer};
|
||||
use ironrdp_graphics::image_processing::PixelFormat;
|
||||
use ironrdp_graphics::rectangle_processing::Region;
|
||||
use ironrdp_graphics::{dwt, quantization, rlgr, subband_reconstruction};
|
||||
use ironrdp_pdu::codecs::rfx::{self, EntropyAlgorithm, Quant, RfxRectangle, Tile};
|
||||
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
|
||||
use ironrdp_pdu::{Decode as _, ReadCursor, decode_cursor};
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
use crate::image::DecodedImage;
|
||||
use crate::{SessionResult, custom_err, general_err, reason_err};
|
||||
|
||||
const TILE_SIZE: u16 = 64;
|
||||
|
||||
pub type FrameId = u32;
|
||||
|
||||
pub struct DecodingContext {
|
||||
context: rfx::ContextPdu,
|
||||
channels: rfx::ChannelsPdu,
|
||||
decoding_tiles: DecodingTileContext,
|
||||
}
|
||||
|
||||
impl Default for DecodingContext {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
context: rfx::ContextPdu {
|
||||
flags: rfx::OperatingMode::empty(),
|
||||
entropy_algorithm: EntropyAlgorithm::Rlgr1,
|
||||
},
|
||||
channels: rfx::ChannelsPdu(Vec::new()),
|
||||
decoding_tiles: DecodingTileContext::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodingContext {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn decode(
|
||||
&mut self,
|
||||
image: &mut DecodedImage,
|
||||
destination: &InclusiveRectangle,
|
||||
input: &mut ReadCursor<'_>,
|
||||
) -> SessionResult<(FrameId, InclusiveRectangle)> {
|
||||
loop {
|
||||
let block = rfx::Block::decode(input).map_err(|e| custom_err!("decode block", e))?;
|
||||
|
||||
match block {
|
||||
rfx::Block::Sync(_) => {
|
||||
self.process_sync(input)?;
|
||||
}
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::FrameBegin(f)) => {
|
||||
return self.process_frame(f, input, image, destination);
|
||||
}
|
||||
_ => {
|
||||
return Err(reason_err!(
|
||||
"rfx::DecodingContext",
|
||||
"unexpected RFX block type: {:?}",
|
||||
block.block_type()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_sync(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
|
||||
self.process_headers(input)
|
||||
}
|
||||
|
||||
fn process_headers(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> {
|
||||
let mut context = None;
|
||||
let mut channels = None;
|
||||
|
||||
// headers can appear in any order: CodecVersions, Channels, Context
|
||||
for _ in 0..3 {
|
||||
match decode_cursor(input).map_err(|e| custom_err!("decode headers", e))? {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::Context(c)) => context = Some(c),
|
||||
rfx::Block::Channels(c) => channels = Some(c),
|
||||
rfx::Block::CodecVersions(_) => (),
|
||||
_ => {
|
||||
return Err(general_err!("unexpected RFX block type"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let context = context.ok_or_else(|| general_err!("context header is missing"))?;
|
||||
let channels = channels.ok_or_else(|| general_err!("channels header is missing"))?;
|
||||
|
||||
if channels.0.is_empty() {
|
||||
return Err(general_err!("no RFX channel announced"));
|
||||
}
|
||||
|
||||
self.context = context;
|
||||
self.channels = channels;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn process_frame(
|
||||
&mut self,
|
||||
frame_begin: rfx::FrameBeginPdu,
|
||||
input: &mut ReadCursor<'_>,
|
||||
image: &mut DecodedImage,
|
||||
destination: &InclusiveRectangle,
|
||||
) -> SessionResult<(FrameId, InclusiveRectangle)> {
|
||||
let channel = self
|
||||
.channels
|
||||
.0
|
||||
.first()
|
||||
.ok_or_else(|| general_err!("no RFX channel found"))?;
|
||||
let width = channel.width.try_into().map_err(|_| general_err!("invalid width"))?;
|
||||
let height = channel.height.try_into().map_err(|_| general_err!("invalid height"))?;
|
||||
let entropy_algorithm = self.context.entropy_algorithm;
|
||||
|
||||
let region: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode region", e))?;
|
||||
let mut region = match region {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::Region(region)) => region,
|
||||
_ => return Err(general_err!("unexpected block type")),
|
||||
};
|
||||
let tile_set: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode tile_set", e))?;
|
||||
let tile_set = match tile_set {
|
||||
rfx::Block::CodecChannel(rfx::CodecChannel::TileSet(t)) => t,
|
||||
_ => return Err(general_err!("unexpected block type")),
|
||||
};
|
||||
let frame_end: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode frame_end", e))?;
|
||||
if !matches!(frame_end, rfx::Block::CodecChannel(rfx::CodecChannel::FrameEnd(_))) {
|
||||
return Err(general_err!("unexpected block type"));
|
||||
}
|
||||
|
||||
if region.rectangles.is_empty() {
|
||||
region.rectangles = vec![RfxRectangle {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
}];
|
||||
}
|
||||
let region = region;
|
||||
|
||||
trace!(frame_index = frame_begin.index);
|
||||
trace!(destination_rectangle = ?destination);
|
||||
trace!(context = ?self.context);
|
||||
trace!(channels = ?self.channels);
|
||||
trace!(?region);
|
||||
|
||||
let clipping_rectangles = clipping_rectangles(region.rectangles.as_slice(), destination, width, height);
|
||||
trace!("Clipping rectangles: {:?}", clipping_rectangles);
|
||||
|
||||
let mut final_update_rectangle = clipping_rectangles.extents.clone();
|
||||
|
||||
for (update_rectangle, tile_data) in tiles_to_rectangles(tile_set.tiles.as_slice(), destination)
|
||||
.zip(map_tiles_data(tile_set.tiles.as_slice(), tile_set.quants.as_slice()))
|
||||
{
|
||||
decode_tile(
|
||||
&tile_data,
|
||||
entropy_algorithm,
|
||||
self.decoding_tiles.tile_output.as_mut(),
|
||||
self.decoding_tiles.ycbcr_buffer.as_mut(),
|
||||
self.decoding_tiles.ycbcr_temp_buffer.as_mut(),
|
||||
)?;
|
||||
|
||||
let current_update_rectangle = image.apply_tile(
|
||||
&self.decoding_tiles.tile_output,
|
||||
PixelFormat::RgbA32,
|
||||
&clipping_rectangles,
|
||||
&update_rectangle,
|
||||
)?;
|
||||
|
||||
final_update_rectangle = final_update_rectangle.union(¤t_update_rectangle);
|
||||
}
|
||||
|
||||
Ok((frame_begin.index, final_update_rectangle))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DecodingTileContext {
|
||||
tile_output: Vec<u8>,
|
||||
ycbcr_buffer: Vec<Vec<i16>>,
|
||||
ycbcr_temp_buffer: Vec<i16>,
|
||||
}
|
||||
|
||||
impl DecodingTileContext {
|
||||
fn new() -> Self {
|
||||
let tile_size = usize::from(TILE_SIZE);
|
||||
Self {
|
||||
tile_output: vec![0; tile_size * tile_size * 4],
|
||||
ycbcr_buffer: vec![vec![0; tile_size * tile_size]; 3],
|
||||
ycbcr_temp_buffer: vec![0; tile_size * tile_size],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_tile(
|
||||
tile: &TileData<'_>,
|
||||
entropy_algorithm: EntropyAlgorithm,
|
||||
output: &mut [u8],
|
||||
ycbcr_temp: &mut [Vec<i16>],
|
||||
temp: &mut [i16],
|
||||
) -> SessionResult<()> {
|
||||
for ((quant, data), ycbcr_buffer) in tile.quants.iter().zip(tile.data.iter()).zip(ycbcr_temp.iter_mut()) {
|
||||
decode_component(quant, entropy_algorithm, data, ycbcr_buffer.as_mut_slice(), temp)?;
|
||||
}
|
||||
|
||||
let ycbcr_buffer = YCbCrBuffer {
|
||||
y: ycbcr_temp[0].as_slice(),
|
||||
cb: ycbcr_temp[1].as_slice(),
|
||||
cr: ycbcr_temp[2].as_slice(),
|
||||
};
|
||||
|
||||
color_conversion::ycbcr_to_rgba(ycbcr_buffer, output).map_err(|e| custom_err!("decode_tile", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_component(
|
||||
quant: &Quant,
|
||||
entropy_algorithm: EntropyAlgorithm,
|
||||
data: &[u8],
|
||||
output: &mut [i16],
|
||||
temp: &mut [i16],
|
||||
) -> SessionResult<()> {
|
||||
rlgr::decode(entropy_algorithm, data, output).map_err(|e| custom_err!("decode_component", e))?;
|
||||
subband_reconstruction::decode(&mut output[4032..]);
|
||||
quantization::decode(output, quant);
|
||||
dwt::decode(output, temp);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clipping_rectangles(
|
||||
rectangles: &[RfxRectangle],
|
||||
destination: &InclusiveRectangle,
|
||||
width: u16,
|
||||
height: u16,
|
||||
) -> Region {
|
||||
let mut clipping_rectangles = Region::new();
|
||||
|
||||
rectangles
|
||||
.iter()
|
||||
.map(|r| InclusiveRectangle {
|
||||
left: min(destination.left + r.x, width - 1),
|
||||
top: min(destination.top + r.y, height - 1),
|
||||
right: min(destination.left + r.x + r.width - 1, width - 1),
|
||||
bottom: min(destination.top + r.y + r.height - 1, height - 1),
|
||||
})
|
||||
.for_each(|r| clipping_rectangles.union_rectangle(r));
|
||||
|
||||
clipping_rectangles
|
||||
}
|
||||
|
||||
fn tiles_to_rectangles<'a>(
|
||||
tiles: &'a [Tile<'_>],
|
||||
destination: &'a InclusiveRectangle,
|
||||
) -> impl Iterator<Item = InclusiveRectangle> + 'a {
|
||||
tiles.iter().map(|t| InclusiveRectangle {
|
||||
left: destination.left + t.x * TILE_SIZE,
|
||||
top: destination.top + t.y * TILE_SIZE,
|
||||
right: destination.left + t.x * TILE_SIZE + TILE_SIZE - 1,
|
||||
bottom: destination.top + t.y * TILE_SIZE + TILE_SIZE - 1,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_tiles_data<'a>(tiles: &[Tile<'a>], quants: &[Quant]) -> Vec<TileData<'a>> {
|
||||
tiles
|
||||
.iter()
|
||||
.map(|t| TileData {
|
||||
quants: [
|
||||
quants[usize::from(t.y_quant_index)].clone(),
|
||||
quants[usize::from(t.cb_quant_index)].clone(),
|
||||
quants[usize::from(t.cr_quant_index)].clone(),
|
||||
],
|
||||
data: [t.y_data, t.cb_data, t.cr_data],
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct TileData<'a> {
|
||||
quants: [Quant; 3],
|
||||
data: [&'a [u8]; 3],
|
||||
}
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
use ironrdp_core::{WriteBuf, decode};
|
||||
use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel};
|
||||
use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage, SendDataIndicationCtx};
|
||||
use ironrdp_pdu::rdp::autodetect::{AutoDetectReqPdu, AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu};
|
||||
use ironrdp_pdu::rdp::headers::ShareDataPdu;
|
||||
use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu;
|
||||
use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu};
|
||||
use ironrdp_pdu::x224::X224;
|
||||
use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages, client_encode_svc_messages};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{SessionError, SessionErrorExt as _, SessionResult, reason_err};
|
||||
|
||||
/// X224 Processor output
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProcessorOutput {
|
||||
/// A buffer with encoded data to send to the server.
|
||||
ResponseFrame(Vec<u8>),
|
||||
/// A graceful disconnect notification. Client should close the connection upon receiving this.
|
||||
Disconnect(DisconnectDescription),
|
||||
/// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the
|
||||
/// [Deactivation-Reactivation Sequence].
|
||||
///
|
||||
/// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432
|
||||
DeactivateAll,
|
||||
/// Server Initiate Multitransport Request. The application should establish a
|
||||
/// sideband UDP transport using the request ID and security cookie, then send
|
||||
/// a [`MultitransportResponsePdu`] back on the IO channel.
|
||||
///
|
||||
/// See [\[MS-RDPBCGR\] 2.2.15.1].
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490
|
||||
/// [`MultitransportResponsePdu`]: ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu
|
||||
MultitransportRequest(MultitransportRequestPdu),
|
||||
/// Auto-detect network characteristics from server ([\[MS-RDPBCGR\] 2.2.14]).
|
||||
///
|
||||
/// Currently only surfaces [`AutoDetectRequest::NetworkCharacteristicsResult`].
|
||||
/// RTT requests are handled internally with automatic responses.
|
||||
///
|
||||
/// [\[MS-RDPBCGR\] 2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dc672839-4f4e-40b1-a71c-cd6a959baa38
|
||||
AutoDetect(AutoDetectRequest),
|
||||
/// Slow-path graphics update ([MS-RDPBCGR] 2.2.9.1.1.3).
|
||||
/// Raw update payload starting with `updateType(u16)`.
|
||||
GraphicsUpdate(Vec<u8>),
|
||||
/// Slow-path pointer update ([MS-RDPBCGR] 2.2.9.1.1.4).
|
||||
/// Raw pointer payload starting with `messageType(u16) + pad(u16)`.
|
||||
PointerUpdate(Vec<u8>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DisconnectDescription {
|
||||
/// Includes the reason from the MCS Disconnect Provider Ultimatum.
|
||||
/// This is the least-specific disconnect reason and is only used
|
||||
/// when a more specific disconnect code is not available.
|
||||
McsDisconnect(DisconnectReason),
|
||||
|
||||
/// Includes the error information sent by the RDP server when there
|
||||
/// is a connection or disconnection failure.
|
||||
ErrorInfo(ErrorInfo),
|
||||
}
|
||||
|
||||
pub struct Processor {
|
||||
static_channels: StaticChannelSet,
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
message_channel_id: Option<u16>,
|
||||
share_id: u32,
|
||||
}
|
||||
|
||||
impl Processor {
|
||||
pub fn new(
|
||||
static_channels: StaticChannelSet,
|
||||
user_channel_id: u16,
|
||||
io_channel_id: u16,
|
||||
message_channel_id: Option<u16>,
|
||||
share_id: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
static_channels,
|
||||
user_channel_id,
|
||||
io_channel_id,
|
||||
message_channel_id,
|
||||
share_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_share_id(&mut self, share_id: u32) {
|
||||
self.share_id = share_id;
|
||||
}
|
||||
|
||||
pub fn get_svc_processor<T: SvcProcessor + 'static>(&self) -> Option<&T> {
|
||||
self.static_channels
|
||||
.get_by_type::<T>()
|
||||
.and_then(|svc| svc.channel_processor_downcast_ref())
|
||||
}
|
||||
|
||||
pub fn get_svc_processor_mut<T: SvcProcessor + 'static>(&mut self) -> Option<&mut T> {
|
||||
self.static_channels
|
||||
.get_by_type_mut::<T>()
|
||||
.and_then(|svc| svc.channel_processor_downcast_mut())
|
||||
}
|
||||
|
||||
/// Completes user's SVC request with data, required to sent it over the network and returns
|
||||
/// a buffer with encoded data.
|
||||
pub fn process_svc_processor_messages<C: SvcProcessor + 'static>(
|
||||
&self,
|
||||
messages: SvcProcessorMessages<C>,
|
||||
) -> SessionResult<Vec<u8>> {
|
||||
let channel_id = self
|
||||
.static_channels
|
||||
.get_channel_id_by_type::<C>()
|
||||
.ok_or_else(|| reason_err!("SVC", "channel not found"))?;
|
||||
|
||||
process_svc_messages(messages.into(), channel_id, self.user_channel_id)
|
||||
}
|
||||
|
||||
pub fn get_dvc<T: DvcProcessor + 'static>(&self) -> Option<&DynamicVirtualChannel> {
|
||||
self.get_svc_processor::<DrdynvcClient>()?.get_dvc_by_type_id::<T>()
|
||||
}
|
||||
|
||||
pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> {
|
||||
self.get_svc_processor::<DrdynvcClient>()?
|
||||
.get_dvc_by_channel_id(channel_id)
|
||||
}
|
||||
|
||||
/// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed
|
||||
/// in the returned order.
|
||||
pub fn process(&mut self, frame: &[u8]) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
let data_ctx: SendDataIndicationCtx<'_> =
|
||||
ironrdp_pdu::mcs::decode_send_data_indication(frame).map_err(SessionError::decode)?;
|
||||
let channel_id = data_ctx.channel_id;
|
||||
|
||||
if channel_id == self.io_channel_id {
|
||||
self.process_io_channel(data_ctx)
|
||||
} else if self.message_channel_id == Some(channel_id) {
|
||||
self.process_message_channel(data_ctx)
|
||||
} else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) {
|
||||
let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?;
|
||||
process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id)
|
||||
.map(|data| vec![ProcessorOutput::ResponseFrame(data)])
|
||||
} else {
|
||||
Err(reason_err!("X224", "unexpected channel received: ID {channel_id}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
debug_assert_eq!(data_ctx.channel_id, self.io_channel_id);
|
||||
|
||||
let io_channel = ironrdp_pdu::rdp::headers::decode_io_channel(data_ctx).map_err(SessionError::decode)?;
|
||||
|
||||
match io_channel {
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::Data(ctx) => {
|
||||
match ctx.pdu {
|
||||
ShareDataPdu::SaveSessionInfo(session_info) => {
|
||||
debug!("Got Session Save Info PDU: {session_info:?}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
// FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU"
|
||||
ShareDataPdu::SetKeyboardIndicators(data) => {
|
||||
debug!("Got Keyboard Indicators PDU: {data:?}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode(
|
||||
ProtocolIndependentCode::None,
|
||||
))) => {
|
||||
debug!("Received None server error");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => {
|
||||
// This is a part of server-side graceful disconnect procedure defined
|
||||
// in [MS-RDPBCGR].
|
||||
//
|
||||
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8
|
||||
let desc = DisconnectDescription::ErrorInfo(e);
|
||||
Ok(vec![ProcessorOutput::Disconnect(desc)])
|
||||
}
|
||||
ShareDataPdu::ShutdownDenied => {
|
||||
debug!("ShutdownDenied received, session will be closed");
|
||||
|
||||
// As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we
|
||||
// need to send a disconnect ultimatum to the server if we want to proceed with the
|
||||
// session shutdown.
|
||||
//
|
||||
// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68
|
||||
let ultimatum = McsMessage::DisconnectProviderUltimatum(
|
||||
DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested),
|
||||
);
|
||||
|
||||
let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode);
|
||||
|
||||
Ok(vec![
|
||||
ProcessorOutput::ResponseFrame(encoded_pdu?),
|
||||
ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect(
|
||||
DisconnectReason::UserRequested,
|
||||
)),
|
||||
])
|
||||
}
|
||||
// TODO: slow-path payloads may be bulk-compressed when
|
||||
// ClientInfoFlags::COMPRESSION is negotiated. Decompression
|
||||
// should happen here before passing data downstream. Currently
|
||||
// IronRDP does not wire bulk decompression into this path.
|
||||
// FIXME: until this is wired, the client deliberately defaults to the simple,
|
||||
// stateless-friendly MPPC 64K (RDP5) compression level rather than XCRUSH; a
|
||||
// stateful codec would risk silent corruption on slow-path updates.
|
||||
ShareDataPdu::Update(data) => {
|
||||
debug!("Got slow-path graphics update ({} bytes)", data.len());
|
||||
Ok(vec![ProcessorOutput::GraphicsUpdate(data)])
|
||||
}
|
||||
ShareDataPdu::Pointer(data) => {
|
||||
debug!("Got slow-path pointer update ({} bytes)", data.len());
|
||||
Ok(vec![ProcessorOutput::PointerUpdate(data)])
|
||||
}
|
||||
_ => Err(reason_err!(
|
||||
"IO channel",
|
||||
"unhandled PDU: {:?}",
|
||||
ctx.pdu.as_short_name()
|
||||
)),
|
||||
}
|
||||
}
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::MultitransportRequest(pdu) => {
|
||||
debug!(
|
||||
"Received Initiate Multitransport Request: request_id={}",
|
||||
pdu.request_id
|
||||
);
|
||||
Ok(vec![ProcessorOutput::MultitransportRequest(pdu)])
|
||||
}
|
||||
ironrdp_pdu::rdp::headers::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an auto-detect request received on the MCS message channel.
|
||||
///
|
||||
/// During continuous auto-detection ([MS-RDPBCGR] 2.2.14) the server sends
|
||||
/// RTT (and bandwidth) requests on the message channel; the client answers
|
||||
/// RTT requests and surfaces the final Network Characteristics Result.
|
||||
fn process_message_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult<Vec<ProcessorOutput>> {
|
||||
let Some(message_channel_id) = self.message_channel_id else {
|
||||
return Err(reason_err!("message channel", "no message channel negotiated"));
|
||||
};
|
||||
|
||||
let req = decode::<AutoDetectReqPdu>(data_ctx.user_data).map_err(SessionError::decode)?;
|
||||
|
||||
match req.request {
|
||||
AutoDetectRequest::RttRequest { sequence_number, .. } => {
|
||||
let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number });
|
||||
let mut frame = WriteBuf::new();
|
||||
ironrdp_pdu::mcs::encode_send_data_request(
|
||||
self.user_channel_id,
|
||||
message_channel_id,
|
||||
&response,
|
||||
&mut frame,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
debug!(sequence_number, "Responded to auto-detect RTT request");
|
||||
Ok(vec![ProcessorOutput::ResponseFrame(frame.into_inner())])
|
||||
}
|
||||
req @ AutoDetectRequest::NetworkCharacteristicsResult { .. } => {
|
||||
debug!(?req, "Received network characteristics from server");
|
||||
Ok(vec![ProcessorOutput::AutoDetect(req)])
|
||||
}
|
||||
req => {
|
||||
debug!(?req, "Auto-detect request not yet implemented");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a pdu on the static global channel. Typically used to send input events
|
||||
pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult<usize> {
|
||||
let written = ironrdp_pdu::rdp::headers::encode_share_data(
|
||||
self.user_channel_id,
|
||||
self.io_channel_id,
|
||||
self.share_id,
|
||||
pdu,
|
||||
output,
|
||||
)
|
||||
.map_err(SessionError::encode)?;
|
||||
Ok(written)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel.
|
||||
///
|
||||
/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer.
|
||||
/// The messages returned here are ready to be sent to the server.
|
||||
///
|
||||
/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel.
|
||||
fn process_svc_messages(messages: Vec<SvcMessage>, channel_id: u16, initiator_id: u16) -> SessionResult<Vec<u8>> {
|
||||
client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode)
|
||||
}
|
||||
Reference in New Issue
Block a user