feat(cliprdr,web,ffi): implement clipboard file transfer support#1166
Conversation
c8ce2e7 to
8111eb2
Compare
|
Thank you!! I appreciate the review hints, this will help with the big diff. I would prefer it if the work can be split a bit narrower, multiple PRs with smaller scopes, but if it's too late it's okay, and I understand it's not always easy to hide all "journey" behind the sausage making. The work seems to still remain focused. I'll start by launching a quick review with Copilot to get rid of the nitpicks! |
There was a problem hiding this comment.
Pull request overview
This PR implements end-to-end clipboard file transfer support (upload + download) over the CLIPRDR channel per MS-RDPECLIP, spanning the Rust protocol implementation, WASM/web bridge, TypeScript client API, FFI bindings, and test/fuzz coverage.
Changes:
- Extends
ironrdp-cliprdrPDUs/state machine for FileGroupDescriptorW, file-contents streaming, capability negotiation, and clipboard lock lifecycle management. - Adds web-facing file transfer APIs (Session/SessionBuilder callbacks +
FileTransferManager), integrates clipboard monitoring suppression during uploads, and introduces Vitest-based tests. - Updates FFI (Rust + generated .NET) to expose new clipboard/file-transfer message variants and adds fuzz targets + expanded testsuite coverage.
Reviewed changes
Copilot reviewed 55 out of 86 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| web-client/iron-remote-desktop/vite.config.ts | Adds Vitest configuration (jsdom, setup file). |
| web-client/iron-remote-desktop/src/test/setup.ts | Vitest global setup hook. |
| web-client/iron-remote-desktop/src/services/remote-desktop.service.ts | Wires file-transfer callbacks + enableFileTransfer() and adds Ctrl+C/Ctrl+V combos. |
| web-client/iron-remote-desktop/src/services/enableFileTransfer.test.ts | Adds integration tests for enableFileTransfer() behavior. |
| web-client/iron-remote-desktop/src/services/clipboard.service.ts | Adds clipboard monitoring suppression during uploads. |
| web-client/iron-remote-desktop/src/services/PublicAPI.ts | Exposes ctrlC/ctrlV + enableFileTransfer with monitoring suppression. |
| web-client/iron-remote-desktop/src/main.ts | Exports file transfer types/APIs for consumers. |
| web-client/iron-remote-desktop/src/interfaces/UserInteraction.ts | Adds ctrlC/ctrlV + enableFileTransfer to the public interface. |
| web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts | Adds file-transfer callback registrations on SessionBuilder. |
| web-client/iron-remote-desktop/src/interfaces/Session.ts | Adds low-level file transfer methods to Session interface + docs. |
| web-client/iron-remote-desktop/src/interfaces/FileTransfer.ts | Introduces file transfer data model types (FileInfo/Request/Response). |
| web-client/iron-remote-desktop/src/enums/SpecialCombination.ts | Adds CTRL_C/CTRL_V. |
| web-client/iron-remote-desktop/src/enums/FileContentsFlags.ts | Adds FileContentsFlags constants and typing. |
| web-client/iron-remote-desktop/package.json | Adds Vitest + jsdom dependencies and scripts. |
| web-client/iron-remote-desktop/README.md | Documents FileTransferManager and low-level APIs. |
| web-client/iron-remote-desktop/.prettierignore | Ignores /pkg output. |
| web-client/iron-remote-desktop-rdp/tsconfig.json | Adjusts TS types and lib checking. |
| web-client/iron-remote-desktop-rdp/package.json | Adds @types/node + normalizes deps. |
| web-client/iron-remote-desktop-rdp/package-lock.json | Lockfile updates for node typings and deps. |
| web-client/README.md | Documents Node prerequisites for web builds. |
| typos.toml | Updates typos-cli dictionary/ignores. |
| fuzz/fuzz_targets/cliprdr_channel_processing.rs | Adds CLIPRDR channel-processing fuzz target. |
| fuzz/Cargo.toml | Registers the new fuzz target binary. |
| ffi/src/session/mod.rs | Uses mutable SVC processor access for clipboard operations. |
| ffi/src/clipboard/windows.rs | Fixes proxy type name + improves mpsc disconnect handling. |
| ffi/src/clipboard/mod.rs | Renames/provides correct FFI clipboard proxy + svc message type. |
| ffi/src/clipboard/message.rs | Exposes new clipboard message variants (unlock, file contents, errors) via Diplomat. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawSessionFfiResultMultitransportRequestBoxIronRdpError.cs | New generated result type for multitransport. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawMultitransportRequest.cs | New raw struct for multitransport request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiLockDataId.cs | New raw handle wrapper for lock data id. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiFileContentsResponse.cs | New raw handle wrapper for file-contents response. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiFileContentsRequest.cs | New raw handle wrapper for file-contents request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectorResultFfiResultU32BoxIronRdpError.cs | New generated result type (u32). |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectionResult.cs | Adds share-id getter to raw ConnectionResult. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectionActivationStateFinalized.cs | Adds share-id getter to raw activation state. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardSvcMessage.cs | Renames raw clipboard svc message wrapper. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessageType.cs | Extends clipboard message type enum for new variants. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessageFfiResultU32BoxIronRdpError.cs | New generated result type for clipboard message API. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessage.cs | Adds new getters for unlock/file-contents/error. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStageOutputType.cs | Adds multitransport output type. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStageOutput.cs | Adds getter for multitransport request output. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStage.cs | Extends fastpath processor setter signature (shareId). |
| ffi/dotnet/Devolutions.IronRdp/Generated/MultitransportRequest.cs | New managed wrapper for multitransport request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiLockDataId.cs | New managed wrapper for lock data id. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiFileContentsResponse.cs | New managed wrapper for file-contents response. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiFileContentsRequest.cs | New managed wrapper for file-contents request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ConnectionResult.cs | Exposes ShareId on managed ConnectionResult. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ConnectionActivationStateFinalized.cs | Exposes ShareId on managed activation state. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardSvcMessage.cs | Renames managed clipboard svc message wrapper. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardMessageType.cs | Extends managed clipboard message type enum. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardMessage.cs | Adds managed properties/getters for new clipboard message variants. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStageOutputType.cs | Adds multitransport output type (managed). |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStageOutput.cs | Adds multitransport request accessor (managed). |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStage.cs | Extends managed fastpath processor setter signature. |
| crates/ironrdp-web/src/session.rs | Wires CLIPRDR file transfer messages between WASM and RDP core; adds tests and safer event sending. |
| crates/ironrdp-web/Cargo.toml | Enables needed web-sys features for timing/window access. |
| crates/ironrdp-testsuite-core/tests/clipboard/mod.rs | Registers new clipboard/file-transfer test modules and updates expectations. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_transfer_capabilities.rs | Adds capability negotiation/encoding tests. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_list_format.rs | Adds file list format and round-trip tests. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_contents_validation.rs | Adds strict spec-validation tests for requests/responses. |
| crates/ironrdp-testsuite-core/tests/clipboard/delayed_rendering_integration.rs | Adds integration tests for delayed rendering file transfer flows. |
| crates/ironrdp-testsuite-core/tests/clipboard/delayed_rendering.rs | Adds unit tests for delayed rendering and boundaries. |
| crates/ironrdp-testsuite-core/Cargo.toml | Adds dependency needed by new tests. |
| crates/ironrdp-server/src/server.rs | Updates clipboard message handling for new enum variants. |
| crates/ironrdp-fuzzing/src/oracles/mod.rs | Adds decoders/oracles for new CLIPRDR PDUs + channel oracle. |
| crates/ironrdp-cliprdr/src/pdu/mod.rs | Adjusts error text for unknown msg types. |
| crates/ironrdp-cliprdr/src/pdu/format_list.rs | Normalizes error message casing. |
| crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs | Adds decode rationale comment re: bounds/borrowing. |
| crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs | Adds relative paths, bounds checks, and stronger decode safety for file lists. |
| crates/ironrdp-cliprdr/src/pdu/file_contents.rs | Renames DATA->RANGE, adds validation, and tightens request/response parsing. |
| crates/ironrdp-cliprdr/src/pdu/capabilities.rs | Fixes downgrade logic and supports unknown protocol versions. |
| crates/ironrdp-cliprdr/src/backend.rs | Extends backend API for file lists, lock cleanup hooks, and timing hooks. |
| crates/ironrdp-cliprdr/README.md | Adds detailed design documentation and usage for file transfer/locking. |
| crates/ironrdp-cliprdr/Cargo.toml | Enables tests for the crate (removes test=false). |
| crates/ironrdp-cliprdr-native/src/windows/cliprdr_backend.rs | Implements monotonic time functions for lock housekeeping. |
| crates/ironrdp-cliprdr-native/src/stub.rs | Implements monotonic time functions for stub backend. |
| crates/ironrdp-cliprdr-native/src/lib.rs | Adds shared monotonic clock helper (native_now_ms). |
| crates/ironrdp-client/src/rdp.rs | Updates clipboard message dispatch for new enum variants. |
| crates/iron-remote-desktop/src/session.rs | Extends WASM-facing Session/Builder traits with file transfer APIs. |
| crates/iron-remote-desktop/src/lib.rs | Exposes new WASM bindings for file transfer methods/callbacks with position validation. |
| Cargo.lock | Adds new crate dependencies to workspace lockfile. |
Files not reviewed (2)
- web-client/iron-remote-desktop-rdp/package-lock.json: Language not supported
- web-client/iron-remote-desktop/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
web-client/iron-remote-desktop/src/services/enableFileTransfer.test.ts
Outdated
Show resolved
Hide resolved
8111eb2 to
3f3e3a4
Compare
|
Also Benoît Cortier (@CBenoit), I'm willing to split this into more-reviewable diffs; it'd be artificially done, since the implementation evolved over time and there are inter-module interface changes and internal deps. Each diff would not necessarily be buildable or testable in isolation. If you can handle the big diff, that'd be less work for me, but I do want to be friendly and share the load, so let me know what you need! |
I think it’s okay; you gave me enough entry points, and I’ll trust you and Copilot on the smaller details. Speaking of which, you don’t need to address every comment it leaves. Sometimes it’s not that great, but surprisingly, it’s getting pretty good at making project-specific suggestions. I’ll run it again because it’s not deterministic and actually catches new stuff with more rounds, especially for large PRs. I can also see you already spend a lot of time thinking and tuning things, the code seems to be high quality even without scrutinizing everything. On my side, I’ll focus on the higher-level aspects, such as the new API for To be honest, since it’s still a big diff, it may take me a bit more time, and there’s definitely a psychological effect that makes the task feel more daunting, but I can handle it 🙂 |
There was a problem hiding this comment.
Pull request overview
Implements end-to-end clipboard-based file transfer over CLIPRDR (per MS-RDPECLIP), spanning the Rust protocol layer, WASM/web bridge, TypeScript API surface, and FFI/.NET bindings, with added fuzzing and test coverage.
Changes:
- Adds CLIPRDR file-transfer capabilities/PDUs (file list + file contents, locking lifecycle, stricter wire validation) and backend hooks.
- Integrates file transfer into the web stack: new session callbacks/methods, a TypeScript
FileTransferManager, and clipboard-monitoring suppression during uploads. - Updates FFI bindings (.NET generated surfaces included), adds fuzz targets, and expands the Rust testsuite coverage for file-transfer and delayed-rendering flows.
Reviewed changes
Copilot reviewed 55 out of 86 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| web-client/iron-remote-desktop/vite.config.ts | Enables Vitest (jsdom + setup file) for the web client package. |
| web-client/iron-remote-desktop/src/test/setup.ts | Adds Vitest global test setup scaffold. |
| web-client/iron-remote-desktop/src/services/remote-desktop.service.ts | Wires FileTransferManager + file-transfer callbacks into session connect lifecycle; adds Ctrl+C/Ctrl+V helpers. |
| web-client/iron-remote-desktop/src/services/enableFileTransfer.test.ts | Adds integration tests for enableFileTransfer() behavior and PublicAPI clipboard monitoring suppression wiring. |
| web-client/iron-remote-desktop/src/services/clipboard.service.ts | Adds monitoring suppression/resume to avoid clobbering file-upload clipboard formats. |
| web-client/iron-remote-desktop/src/services/PublicAPI.ts | Exposes ctrlC/ctrlV + enableFileTransfer; composes upload hooks with clipboard suppression. |
| web-client/iron-remote-desktop/src/main.ts | Exports new file-transfer/public API types and helpers. |
| web-client/iron-remote-desktop/src/interfaces/UserInteraction.ts | Extends public interaction contract with ctrlC/ctrlV and enableFileTransfer. |
| web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts | Adds file-transfer callback registration methods to builder interface. |
| web-client/iron-remote-desktop/src/interfaces/Session.ts | Adds file-transfer session methods (lock/unlock, request/submit contents, initiate copy). |
| web-client/iron-remote-desktop/src/interfaces/FileTransfer.ts | Introduces TS types for file metadata and file contents request/response. |
| web-client/iron-remote-desktop/src/enums/SpecialCombination.ts | Adds CTRL_C / CTRL_V to special-combination enum. |
| web-client/iron-remote-desktop/src/enums/FileContentsFlags.ts | Adds TS FileContentsFlags constants/types. |
| web-client/iron-remote-desktop/package.json | Adds test scripts and Vitest/jsdom deps. |
| web-client/iron-remote-desktop/README.md | Documents FileTransferManager usage + low-level APIs; updates limitations section. |
| web-client/iron-remote-desktop/.prettierignore | Ignores /pkg output directory. |
| web-client/iron-remote-desktop-rdp/tsconfig.json | Adjusts TS types config (ua-parser-js) and enables skipLibCheck. |
| web-client/iron-remote-desktop-rdp/package.json | Adds @types/node and adjusts deps ordering (ua-parser-js). |
| web-client/iron-remote-desktop-rdp/package-lock.json | Lockfile updates for new TS/Node typings deps. |
| web-client/README.md | Adds Node prerequisite note for the web workspace. |
| typos.toml | Adjusts typos config to avoid false positives (extend-words). |
| fuzz/fuzz_targets/cliprdr_channel_processing.rs | Adds a libFuzzer target for CLIPRDR channel processing. |
| fuzz/Cargo.toml | Registers the new fuzz target binary. |
| ffi/src/session/mod.rs | Switches to mutable SVC processor access for CLIPRDR actions. |
| ffi/src/clipboard/windows.rs | Fixes proxy type name and improves mpsc disconnected handling. |
| ffi/src/clipboard/mod.rs | Renames/cleans up clipboard message proxy + Diplomat type name. |
| ffi/src/clipboard/message.rs | Expands clipboard message FFI accessors for unlock + file contents + error. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawSessionFfiResultMultitransportRequestBoxIronRdpError.cs | Adds generated .NET raw result for multitransport request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawMultitransportRequest.cs | Adds generated .NET raw multitransport struct. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiLockDataId.cs | Adds generated .NET raw lock ID handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiFileContentsResponse.cs | Adds generated .NET raw file contents response handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawFfiFileContentsRequest.cs | Adds generated .NET raw file contents request handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectorResultFfiResultU32BoxIronRdpError.cs | Adds generated .NET raw result wrapper for u32 returns. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectionResult.cs | Exposes share_id getter in generated raw connection result. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawConnectionActivationStateFinalized.cs | Exposes share_id getter in generated raw activation state. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardSvcMessage.cs | Renames generated clipboard SVC message type. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessageType.cs | Updates generated clipboard message type enum variants for file transfer. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessageFfiResultU32BoxIronRdpError.cs | Adds generated result wrapper used by file contents request DataId accessor. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawClipboardMessage.cs | Adds generated raw accessors for unlock/file-contents/error variants. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStageOutputType.cs | Adds MultitransportRequest output type. |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStageOutput.cs | Adds accessor for multitransport request (without sensitive cookie). |
| ffi/dotnet/Devolutions.IronRdp/Generated/RawActiveStage.cs | Updates fastpath processor setter signature to include shareId. |
| ffi/dotnet/Devolutions.IronRdp/Generated/MultitransportRequest.cs | Adds managed wrapper for multitransport request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiLockDataId.cs | Adds managed wrapper for lock data ID handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiFileContentsResponse.cs | Adds managed wrapper for file contents response handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/FfiFileContentsRequest.cs | Adds managed wrapper for file contents request handle. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ConnectionResult.cs | Exposes ShareId in managed ConnectionResult wrapper. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ConnectionActivationStateFinalized.cs | Exposes ShareId in managed activation state wrapper. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardSvcMessage.cs | Renames managed clipboard SVC message wrapper. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardMessageType.cs | Updates managed clipboard message type enum variants for file transfer. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ClipboardMessage.cs | Adds managed accessors for file-transfer variants and error. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStageOutputType.cs | Adds MultitransportRequest output type (managed). |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStageOutput.cs | Adds managed accessor for multitransport request. |
| ffi/dotnet/Devolutions.IronRdp/Generated/ActiveStage.cs | Updates managed fastpath processor setter signature to include shareId. |
| crates/ironrdp-web/src/session.rs | Adds JS callback plumbing + session methods for file transfer, clipboard lock orchestration, and safer marshalling; adds tests. |
| crates/ironrdp-web/Cargo.toml | Enables needed web-sys features for timing/window integration. |
| crates/ironrdp-testsuite-core/tests/clipboard/mod.rs | Adds new clipboard/file-transfer test modules and updates existing expectations. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_transfer_capabilities.rs | Adds capability negotiation/encoding tests for file transfer flags + versions. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_list_format.rs | Adds packed file list and file descriptor format tests. |
| crates/ironrdp-testsuite-core/tests/clipboard/file_contents_validation.rs | Adds decode/validation tests for file contents requests/responses per spec. |
| crates/ironrdp-testsuite-core/tests/clipboard/delayed_rendering_integration.rs | Adds integration coverage for delayed rendering flows incl. file lists. |
| crates/ironrdp-testsuite-core/tests/clipboard/delayed_rendering.rs | Adds unit tests for delayed rendering behavior, boundaries, and capability gating. |
| crates/ironrdp-testsuite-core/Cargo.toml | Adds ironrdp-svc dependency for clipboard integration tests. |
| crates/ironrdp-server/src/server.rs | Updates clipboard backend message handling for new unlock/file-transfer message shapes. |
| crates/ironrdp-fuzzing/src/oracles/mod.rs | Extends decode fuzzing and adds a CLIPRDR channel processing oracle. |
| crates/ironrdp-cliprdr/src/pdu/mod.rs | Tweaks unknown PDU error string. |
| crates/ironrdp-cliprdr/src/pdu/format_list.rs | Tweaks invalid msgFlags error string. |
| crates/ironrdp-cliprdr/src/pdu/format_data/mod.rs | Adds rationale comment about bounds/borrowing for format data payloads. |
| crates/ironrdp-cliprdr/src/pdu/format_data/file_list.rs | Adds relative path support, count/length hardening, and descriptor builder APIs. |
| crates/ironrdp-cliprdr/src/pdu/file_contents.rs | Renames DATA->RANGE, validates flags/constraints, and tightens request/response parsing. |
| crates/ironrdp-cliprdr/src/pdu/capabilities.rs | Fixes flag negotiation bug and preserves unknown protocol versions. |
| crates/ironrdp-cliprdr/src/backend.rs | Extends backend contract/messages for file lists and lock cleanup events; refines semantics docs. |
| crates/ironrdp-cliprdr/README.md | Adds comprehensive CLIPRDR + file transfer design/API documentation. |
| crates/ironrdp-cliprdr/Cargo.toml | Enables crate tests by removing test = false. |
| crates/ironrdp-cliprdr-native/src/windows/cliprdr_backend.rs | Implements monotonic clock hooks for lock timeout tracking. |
| crates/ironrdp-cliprdr-native/src/stub.rs | Implements monotonic clock hooks for stub backend. |
| crates/ironrdp-cliprdr-native/src/lib.rs | Adds a shared native monotonic clock helper (native_now_ms). |
| crates/ironrdp-client/src/rdp.rs | Switches to mutable CLIPRDR processor access and updates unlock message handling. |
| crates/iron-remote-desktop/src/session.rs | Extends WASM-facing session traits with file-transfer callbacks/methods (default unimplemented). |
| crates/iron-remote-desktop/src/lib.rs | Exposes new WASM bindings for file transfer session APIs and callbacks; validates numeric marshalling. |
| Cargo.lock | Adds ironrdp-svc as a dependency where needed. |
Files not reviewed (2)
- web-client/iron-remote-desktop-rdp/package-lock.json: Language not supported
- web-client/iron-remote-desktop/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
web-client/iron-remote-desktop/src/interfaces/SessionBuilder.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
praise: Even the state machine was fuzzed 👏
|
Nice work on this. Given that I maintain a server-side IronRDP implementation (CliprdrBackend consumer on the Linux/Wayland side), I wanted to share some observations from that perspective. Default trait impls: the approach of adding Path sanitization at the protocol layer: we have ~550 lines of path sanitization on the server side (Linux-specific filename concerns, Windows reserved names, file URI parsing). Having LockStrategy question: what informed the choice of Server-side use case: on our end, the "remote" side is the RDP client advertising files for paste into the Linux desktop. We use a FUSE virtual filesystem so Linux apps can read clipboard files on demand -- each FUSE These are the things that jump out at me. I'm sure there are others.. Greg Lamberson |
|
Hi Greg Lamberson (@glamberson)! Thanks for the kind words; good to see a fellow linux-desktop person on here, and glad this works out-of-the-box for you.
Good call-out. You've been in the trenches with RDP for awhile; can you think of any reason not to nuke |
a851470 to
c86f040
Compare
|
Thinking about this more I'm leaning towards removing Locks are per-clipboard-snapshot, not per-file. The Lock/Unlock PDUs in MS-RDPECLIP 1.3.2.3 lock the entire clipboard state so it doesn't change during reads. There's no scenario where you'd want to lock "one file" - the OnDemand creates redundant locks. With OnDemand, the TypeScript FileTransferManager acquires a separate lock per download, even though all downloads reference the same clipboard state. The server has to track N locks when 1 suffices. Proactive already handles clipboard changes correctly by detaching the old lock (keeping it alive for in-flight transfers) and creating a new one. The TS layer is reimplementing Proactive manually. FileTransferManager has a FreeRDP uses proactive locking, and it's what Windows RDP servers expect per the spec's Figure 3 sequence diagram. Server-side consumers (FUSE, etc.) also benefit. Proactive means the lock is already held when I'll remove Tagging Greg Lamberson (@glamberson) since you raised the question - any concerns with this from the server side? |
|
Gabriel Bauman (@gabrielbauman) No concerns from the server side. This is the right move. The per-snapshot semantics are the key point. With Proactive, the lock is already held when One thing worth considering: server-side backends may not request file contents immediately. A backend might expose the file list to the host OS and defer the actual FileContentsRequests until an application reads the data, potentially minutes later. The lock needs to stay alive until the backend explicitly releases it or the clipboard changes via a new FormatList. As long as the auto-detach on new FormatList keeps the old lock alive for in-flight transfers (which it sounds like it does) this works well for deferred-read patterns. Agreed on nuking the enum. Proactive matches the spec's Figure 3 sequence, matches FreeRDP, and removes a configuration surface that nobody should need to change. Greg |
bcba7e5 to
fd29fef
Compare
|
Okay, I've nuked the enum and made proactive locking the default and only lock strategy. I've also tested with our internal browser RDP client and the new locking works flawlessly against a Windows rdp server. Nice drop in complexity in the wasm and typescript layers. I've also done some drive-by fixes for a few minor style and quality-of-life issues etc. I'm done tweaking until I hear back on the review front. Sorry for the churn! We're in good shape now, I think. Thanks for the discussion, Greg Lamberson (@glamberson)! Have a nice weekend, everyone. |
fd29fef to
68f41c3
Compare
|
Found and fixed a regression from our locking refactor above. |
0224c40 to
bbd1bb4
Compare
|
Hi Benoît Cortier (@CBenoit), anything I can help with? |
There was a problem hiding this comment.
Hi!
I’m really sorry for the delay!
The bottom line for me: the final handling seems to be the right one. Thank you Greg Lamberson (@glamberson) for also double checking the protocol part, and suggesting proactive clipboard locking. That’s a valid and pragmatic choice. I totally endorse the direction.
I have extra remarks and suggestions before we merge.
praise: I think FileTransferManager is a good, high-level abstraction, well-designed. Very nice. We should take inspiration from it in the future to provide a nice higher interface to the consumers.
praise: Well documented.
praise: Thank you for following IronRDP’s STYLE.md!
praise: Test coverage is excellent.
| bench = false | ||
|
|
||
| [[bin]] | ||
| name = "cliprdr_channel_processing" |
There was a problem hiding this comment.
suggestion: Can you add this target in the fuzz workflow too?
There was a problem hiding this comment.
IronRDP/.github/workflows/fuzz.yml
Line 44 in a6b4109
There was a problem hiding this comment.
Will do
There was a problem hiding this comment.
Done (was addressed in a previous push).
crates/ironrdp-cliprdr/README.md
Outdated
| For advanced use cases where you need explicit control, you can call `cleanup_expired_locks()` manually: | ||
|
|
||
| ```rust | ||
| // Optional: Force cleanup check before automatic interval |
There was a problem hiding this comment.
suggestion: I think this wording is easier to understand (at least for me)
| // Optional: Force cleanup check before automatic interval | |
| // Optional: Trigger cleanup check before automatic interval |
| #### Automatic Lock Behavior | ||
|
|
||
| When a FormatList containing `FileGroupDescriptorW` is received and `CAN_LOCK_CLIPDATA` was negotiated, the cliprdr processor automatically: | ||
|
|
||
| 1. Sends a Lock Clipboard Data PDU with a new `clipDataId` | ||
| 2. Passes the `clipDataId` to the backend via `on_remote_file_list(files, clip_data_id)` | ||
| 3. Manages the lock lifecycle (expiry, cleanup, Unlock PDUs) internally | ||
|
|
||
| Backends receive the `clipDataId` and can pass it to `request_file_contents()` via the `data_id` field. No explicit lock/unlock calls are needed. |
There was a problem hiding this comment.
issue: The lazy piggyback pattern is a pragmatic shortcut, but it has real costs in my opinion.
rationale:
- It forces
&mut selfon semantically read-only operations (initiate_paste,submit_format_data), so callers and type system can no longer distinguish between "mutating" and "just builds a PDU". - The return type becomes semantically muddled:
initiate_pastemay return[UNLOCK_CLIPDATA, FORMAT_DATA_REQUEST], which can be surprising, it’s documented but it’s unrelated side-effects bundled in. - Cleanup is only triggered by user activity, and is unpredictable, although I understand it’s not a big issue in practice.
suggestion: I think there is a more idiomatic alternative here. I’m thinking about a dedicated poll/drive called from the event loop. I’ve seen this pattern in crates such as rustls, quinn or tokio. Some return a Duration hint to let the caller scheduler the next drive without polling blindly. In our case, I think we don’t really need much more than an interval though:
// A persistent interval stream.
let cleanup_interval_ms = 5_000;
let mut cleanup_timer = gloo_timers::future::IntervalStream::new(cleanup_interval_ms);
let disconnect_reason = 'outer: loop {
let outputs = select! {
/* ... */
_ = cleanup_timer.next() => {
if let Some(cliprdr) = active_stage.get_svc_processor_mut::<CliprdrClient>() {
match cliprdr.cleanup_expired_locks() {
Ok(svc_messages) => {
let frame = active_stage.process_svc_processor_messages(svc_messages)?;
vec![ActiveStageOutput::ResponseFrame(frame)]
}
Err(e) => {
error!(error = %e, "Clipboard lock cleanup failed");
Vec::new()
}
}
} else {
Vec::new()
}
}
};
};That way Cliprdr can drop the internal auto_cleanup_interval / last_cleanup_check / maybe_cleanup_locks machinery entirely and the scheduling responsibility moves to the caller, making it configurable in the session setup code without coupling it to the protocol processor.
With that in place:
initiate_copy,initiate_paste,submit_format_datago back to&self(they just encode PDUs)maybe_cleanup_locks()disappears entirelycleanup_expired_locks()is the single, explicit, testable cleanup pathget_svc_processorsuffices for most call sites; only genuinely mutating operations (request_file_contents,initiate_file_copy) needget_svc_processor_mut
There was a problem hiding this comment.
I've gone ahead and done this. Will push shortly.
There was a problem hiding this comment.
Done. maybe_cleanup_locks/auto_cleanup_interval/last_cleanup_check removed. drive_timeouts() is the dedicated public method, called from a 5-second interval timer in the web session. submit_format_data takes &self.
initiate_copy and initiate_paste still take &mut self - this is not a cleanup side-effect but genuine state tracking: initiate_copy manages local_file_list, initiate_paste tracks pending_format_data_request for response correlation. Both have inline doc comments explaining why.
| /// Called when outgoing clipboard locks are automatically cleared. | ||
| /// | ||
| /// This is triggered when a new FormatList PDU is received, indicating that the clipboard | ||
| /// content has changed. Per [MS-RDPECLIP] 2.2.4.2, all existing locks are no longer | ||
| /// relevant and must be released. | ||
| /// | ||
| /// **Use case**: Backends can use this to clean up any associated lock state, cancel | ||
| /// ongoing downloads, or update UI to reflect that the remote clipboard has changed. | ||
| /// | ||
| /// ## Parameters | ||
| /// | ||
| /// - `clip_data_ids`: List of clipDataIds for locks that were automatically cleared. | ||
| /// | ||
| /// **Note**: Unlock PDUs are automatically sent for each cleared lock. | ||
| fn on_outgoing_locks_cleared(&mut self, clip_data_ids: &[LockDataId]) { | ||
| let _ = clip_data_ids; | ||
| // Default implementation does nothing - backends can override to handle lock cleanup | ||
| } |
There was a problem hiding this comment.
issue: The trigger description here is wrong, and it conflicts with the on_outgoing_locks_expired doc directly below.
It says "triggered when a new FormatList PDU is received", but that is precisely what triggers on_outgoing_locks_expired. on_outgoing_locks_cleared is triggered later, by cleanup_expired_locks(), after the inactivity/max-lifetime timeout elapses on an expired lock.
Additionally, the spec citation [MS-RDPECLIP] 2.2.4.2 is the structure definition of CLIPRDR_UNLOCK_CLIPDATA, just a PDU layout, and it contains no directive about when locks must be released on clipboard change.
There was a problem hiding this comment.
Fixed.
crates/ironrdp-cliprdr/README.md
Outdated
| **Maximum Lifetime (default: 2 hours)** | ||
| - Locks are force-cleaned after 2 hours regardless of activity | ||
| - This prevents indefinite resource accumulation from slow or stalled transfers | ||
| - Measured from when the lock transitioned to Expired state (not from lock creation) |
There was a problem hiding this comment.
issue: It seems this line contradicts the code, cleanup_expired_locks_impl computes total_lifetime_ms = now.saturating_sub(lock.created_at_ms).
There was a problem hiding this comment.
Fixed.
| /// // Then request data in chunks | ||
| /// requestFileContents(1, 0, 0x2, 0, 4096, clipDataId); | ||
| /// // Wait for fileContentsResponseCallback with data | ||
| /// unlockClipboard(clipDataId); |
There was a problem hiding this comment.
issue: I don’t see this function (or maybe I’m missing it?) (EDIT: I think it was part of the initial implementation)
There was a problem hiding this comment.
Removed.
| // Lock clipboard for file transfer | ||
| const clipDataId = await session.lockClipboard(); | ||
|
|
||
| // Request file size | ||
| session.requestFileContents(streamId, fileIndex, FileContentsFlags.SIZE, 0, 8, clipDataId); | ||
|
|
||
| // Request file data | ||
| session.requestFileContents(streamId, fileIndex, FileContentsFlags.RANGE, offset, chunkSize, clipDataId); | ||
|
|
||
| // Unlock clipboard when done | ||
| session.unlockClipboard(clipDataId); |
There was a problem hiding this comment.
issue: Same as above, lockClipboard() and unlockClipboard() were part of the initial iteration, before the proactive locking refactor.
There was a problem hiding this comment.
Removed.
| * @param flags - FileContentsFlags: 0x1 (SIZE) or 0x2 (RANGE) | ||
| * @param position - Byte offset for RANGE requests | ||
| * @param size - Number of bytes requested for RANGE requests | ||
| * @param clipDataId - Optional lock ID from lockClipboard() |
There was a problem hiding this comment.
issue: Another reference to lockClipboard()
There was a problem hiding this comment.
Removed.
|
I have more suggestions but it’s getting late so I’ll wrap up the review tomorrow! |
Benoît Cortier (CBenoit)
left a comment
There was a problem hiding this comment.
Hi again!
For this second pass, I’m focusing on the new API surface.
It looks like it was not clearly written anywhere, so I opened a PR to document the API design philosophy of iron-remote-desktop: #1192
There was a problem hiding this comment.
issue: The Session/SessionBuilder traits (both Rust and TypeScript) now expose raw RDPECLIP wire concepts: streamId, FileContentsFlags, clipDataId, etc. These don't exist in any other remote protocol and violate the design invariant that iron-remote-desktop is protocol-agnostic.
suggestion: Push requestFileContents, submitFileContents, etc, into the RDP backend layer and wire them through the existing invokeExtension / extension mechanism. For that, you can refer to existing extensions such as displayControl or kdcProxyUrl.
You should use the extension_match! macro to match on the extension, and extract the value. The current extensions are trivial, only using primitive types. In this case, you will need to use js_sys::Reflect module in order to extract the parameters from the JavaScript object / JsValue, and Function::try_from_js_value to convert the JsValue into a js_sys::Function.
Example:
fn invoke_extension(&self, ext: Extension) -> Result<JsValue, Self::Error> {
use js_sys::{JsString, Object};
use wasm_bindgen::convert::TryFromJsValue as _;
iron_remote_desktop::extension_match! {
match ext;
|request_file_contents: JsValue| {
let obj = into_object(request_file_contents)?;
let stream_id = get_u32(&obj, "stream_id")?;
let file_index = get_i32(&obj, "file_index")?;
let flags = get_u32(&obj, "flags")?;
let position = get_u64(&obj, "position")?;
let size = get_u32(&obj, "size")?;
let clip_data_id = get_u32_opt(&obj, "clip_data_id")?;
self.input_events_tx
.unbounded_send(RdpInputEvent::ClipboardBackend(
WasmClipboardBackendMessage::FileContentsRequestSend {
stream_id,
index: file_index,
flags: FileContentsFlags::from_bits_truncate(flags),
position,
size,
clip_data_id,
},
))
.context("send file contents request")
.map_err(Into::into)?;
return Ok(JsValue::NULL);
};
}
return Err(
IronError::from(anyhow::Error::msg(format!("unknown extension: {}", ext.ident())))
.with_kind(IronErrorKind::General),
);
fn into_object(val: JsValue) -> Result<Object, IronError> {
let obj = Object::try_from_js_value(val).ok().context("invalid value")?;
Ok(obj)
}
fn get_u32(obj: &Object, key: &str) -> Result<u32, IronError> {
let property = js_sys::Reflect::get_str(obj, &JsString::from(key))
.map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?
.with_context(|| format!("object is missing property `{key}`"))?;
let value = property
.as_f64()
.map(f64_to_u32_saturating_cast)
.with_context(|| format!("invalid type for property `{key}`"))?;
Ok(value)
}
fn get_u64(obj: &Object, key: &str) -> Result<u64, IronError> {
let property = js_sys::Reflect::get_str(obj, &JsString::from(key))
.map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?
.with_context(|| format!("object is missing property `{key}`"))?;
let value = property
.as_f64()
.map(f64_to_u64_saturating_cast)
.with_context(|| format!("invalid type for property `{key}`"))?;
Ok(value)
}
fn get_i32(obj: &Object, key: &str) -> Result<i32, IronError> {
let property = js_sys::Reflect::get_str(obj, &JsString::from(key))
.map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?
.with_context(|| format!("object is missing property `{key}`"))?;
let value = property
.as_f64()
.map(f64_to_i32_saturating_cast)
.with_context(|| format!("invalid type for property `{key}`"))?;
Ok(value)
}
fn get_u32_opt(obj: &Object, key: &str) -> Result<Option<u32>, IronError> {
match js_sys::Reflect::get_str(obj, &JsString::from(key))
.map_err(|e| anyhow::anyhow!("get property `{key}`: {e:?}"))?
{
Some(property) => {
let value = property
.as_f64()
.map(f64_to_u32_saturating_cast)
.with_context(|| format!("invalid type for property `{key}`"))?;
Ok(Some(value))
}
None => Ok(None),
}
}
}We may refactor the helpers, but it would be good enough as-is for now!
There was a problem hiding this comment.
I see what you mean. Let me poke some more.
There was a problem hiding this comment.
Done. File transfer methods (requestFileContents, submitFileContents, initiateFileCopy) removed from the Session trait and routed through invoke_extension() with js_sys::Reflect parameter extraction. The six file transfer callbacks removed from SessionBuilder and routed through extension() with JsValue -> js_sys::Function conversion via dyn_into. Helper functions (into_object, get_u32, get_u64, etc.) follow the pattern from your example code.
crates/ironrdp-web/src/session.rs
Outdated
| clip_data_id, | ||
| }, | ||
| )) | ||
| .context("Send file contents request") |
There was a problem hiding this comment.
style:
| .context("Send file contents request") | |
| .context("send file contents request") |
There was a problem hiding this comment.
Fixed. Also lowercased the other .context() strings we introduced ("cliprdr initiate copy", "send clipboard backend event", etc.).
There was a problem hiding this comment.
praise: I really like this high-level abstraction.
suggestion: Based on my previous suggestion, this should go into iron-remote-desktop-rdp because it will rely on the new RDP extensions.
thought: At some point in the future we may consider writing a common abstraction using some form of dependency injection again, but I think it’s not necessary for now.
There was a problem hiding this comment.
Noted, will moveit. I'm removing the AbortController and cancellation support because we're finding some RDP servers hate it and have user-facing crashes on cancellation.
There was a problem hiding this comment.
Moved. FileTransferManager is now RdpFileTransferProvider in iron-remote-desktop-rdp. It implements a new protocol-agnostic FileTransferProvider interface defined in iron-remote-desktop. Session interaction uses invokeExtension() exclusively. Extension factory functions (filesAvailableCallback, requestFileContents, etc.) are exported from the rdp package. The web component's enableFileTransfer(provider) accepts any FileTransferProvider via dependency injection - no direct import of the RDP package needed.
| /** | ||
| * Optional | ||
| * | ||
| * Called when remote locks their clipboard for file transfer. The dataId associates |
There was a problem hiding this comment.
issue: I think this is the other way around?
| * Called when remote locks their clipboard for file transfer. The dataId associates | |
| * Called when remote locks our clipboard for file transfer. The dataId associates |
There was a problem hiding this comment.
Fixed. The six callback methods have been removed from SessionBuilder.ts entirely as part of the extension refactor, so the incorrect doc is gone. The callbacks are now registered via builder.extension(lockCallback(cb)) from the RDP package.
crates/ironrdp-cliprdr/src/lib.rs
Outdated
| // SAFETY (ordering): `handle_server_capabilities()` runs | ||
| // synchronously during `process()` for the Capabilities PDU, | ||
| // which always precedes the Monitor Ready PDU in the server's | ||
| // initialization sequence. By the time any backend callback | ||
| // triggers `initiate_copy()`, `self.capabilities` has already | ||
| // been downgraded against the server's capabilities. This holds | ||
| // regardless of whether the backend responds synchronously or | ||
| // asynchronously, because `process()` for the Capabilities PDU | ||
| // completes before `process()` for Monitor Ready begins. |
There was a problem hiding this comment.
nitpick: I think it may be best to keep SAFETY: comments for unsafe blocks, as a convention.
There was a problem hiding this comment.
Changed to INVARIANT (ordering): to match the project convention.
| /// [2.2.5.3]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpeclip/cbc851d3-4e68-45f4-9292-26872a9209f2 | ||
| pub fn request_file_contents(&self, request: FileContentsRequest) -> PduResult<CliprdrSvcMessages<R>> { | ||
| ready_guard!(self, request_file_contents); | ||
| pub fn request_file_contents(&mut self, mut request: FileContentsRequest) -> PduResult<CliprdrSvcMessages<R>> { |
There was a problem hiding this comment.
suggestion: It could be good to enforce the CB_HUGE_FILE_SUPPORT_ENABLED (0x00000020) constraint on nPositionHigh and nPositionLow. What do you think?
There was a problem hiding this comment.
Makes sense!
There was a problem hiding this comment.
Done. Positions >= 2^31 are rejected with a descriptive error when CB_HUGE_FILE_SUPPORT_ENABLED is not negotiated.
| } | ||
|
|
||
| // [MS-RDPECLIP] 2.2.5.3 - Validate flags are spec-compliant | ||
| if let Err(e) = request.flags.validate() { |
There was a problem hiding this comment.
nitpick: Just a note about "Parse Don’t Validate" pattern (https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/); but it’s probably fine as-is as well
There was a problem hiding this comment.
Done. FileTransferState.file_index is now usize. The i32 from the wire is validated via try_from and the resulting usize is stored directly. Doc comment explains the choice.
| /// These methods are gated behind the `__test` feature and exist solely | ||
| /// so that tests in `ironrdp-testsuite-core` can set up / inspect internal | ||
| /// fields without making them part of the public API. | ||
| #[cfg(feature = "__test")] |
There was a problem hiding this comment.
suggestion: Also add #[doc(hidden)] so it’s never shown in docs.rs
| #[cfg(feature = "__test")] | |
| #[cfg(feature = "__test")] | |
| #[doc(hidden)] |
There was a problem hiding this comment.
Done.
crates/ironrdp-cliprdr/src/lib.rs
Outdated
| // Build the effective format list, merging file formats if an upload is active. | ||
| // Per [MS-RDPECLIP] 3.1.5.2.1, the sender "MUST enumerate all of the formats | ||
| // currently available." Per section 1.3.1, mixed file + non-file format lists | ||
| // are valid. Each FormatList completely replaces the previous one (section 3.1.1.1), | ||
| // so we must re-include the file format to keep it visible to the remote. | ||
| let effective_formats = | ||
| if let (Some(_file_list), Some(file_format_id)) = (&self.local_file_list, self.local_file_list_format_id) { | ||
| // A file upload is in progress - preserve the file list and merge | ||
| // FileGroupDescriptorW into the outgoing FormatList. | ||
| warn!("Clipboard text/image update during active file upload; merging file format into FormatList"); | ||
| let file_format = ClipboardFormat::new(file_format_id).with_name(ClipboardFormatName::FILE_LIST); | ||
| let mut merged = available_formats.to_vec(); | ||
| merged.push(file_format); | ||
| merged | ||
| } else { | ||
| // No file upload active - clear file list state as before | ||
| self.local_file_list = None; | ||
| self.local_file_list_format_id = None; | ||
| available_formats.to_vec() | ||
| }; | ||
|
|
There was a problem hiding this comment.
I think there is a bug here. I've been running into an issue when testing policy enforcement on our end.
Observations:
- C2S text clipboard deny text stops appearing after a successful file upload under a mixed policy (file transfer allowed, text clipboard blocked).
It seems that local_file_list (set by initiate_file_copy()), but is only cleared in two places:
FormatListResponse::Failhandling inhandle_format_list_response- The
elsebranch of the highlighted code block (which only runs whenlocal_file_listis alreadyNone).
Issue: After a successful file upload completes, local_file_list remains Some(...) indefinitely. This causes issues in our proxy when dealing with hybrid policy case since we have to send FileGroupDescriptorW for correctness even though user is not really doing a file upload. This prevents the server from seeing the CF_UNICODETEXT format in the FormatList and requesting a paste (to see the deny text).
Two possible fixes:
-
Clear
local_file_listininitiate_copyunconditionally, removing the merge branch entirely. When the user copies new non-file content, the file upload is effectively "done" from the user's perspective. The old file list should be discarded. This is the simplest fix but does mean a concurrent file download-in-progress would be interrupted by a text copy. -
Add transfer-completion detection; track whether the server has finished downloading (not sure how though? do we have a signal for this?). Clear
local_file_listonce the transfer is complete.
Fix (1) seems the least complex and feels correct since the CLIPRDR spec states: "The Clipboard Format ID Map is cleared and initialized whenever a Format List PDU is processed (section 3.1.5.2.2)." The user explicitly copying new content is a strong signal that the previous clipboard state (including file lists) should be replaced.
There was a problem hiding this comment.
Fixed. initiate_copy() now clears local_file_list unconditionally. The old merge branch that preserved the file list during active uploads is removed. Per [MS-RDPECLIP] 3.1.1.1, each FormatList completely replaces the previous. A text/image copy during an active file download ends the upload's visibility to the remote - an acceptable tradeoff since the user explicitly chose new content. Inline comment documents this decision.
| // Suppress clipboard monitoring while the upload is in flight so the | ||
| // 100ms polling loop does not clobber our FormatList with a text/image update. | ||
| this.onUploadStarted?.(); |
There was a problem hiding this comment.
I think this code is causing issues in our policy enforcement for the case of a deny-all policy (both text and file transfer blocked in both directions).
Observations:
- Under a deny-all policy (both text and file transfer blocked in both directions), C2S text clipboard deny text stops appearing after a file upload attempt. This also affects any mixed policy where file transfer is blocked in the relevant direction (see other thread).
- The same issue occurs whether files are uploaded via
showFilePicker()(upload button) or drag-and-drop; both paths go throughuploadFiles().
Sequence of events:
- C2S text copy-paste: deny text appears correctly.
- User uploads files:
uploadFiles()callsthis.onUploadStarted?.(), which callsclipboardService.suppressMonitoring(), settingmonitoringSuppressed = true. The clipboard monitoring loop (clipboard.service.ts::onMonitorClipboard()) now returns immediately on every poll without reading the browser clipboard. initiateFileCopy()sends aFormatListwithFileGroupDescriptorWto the server. Our proxy (enforcing deny-all) replaces the entireFormatListwith aCF_UNICODETEXTdeny placeholder. The server never learns about file formats.- Because the server doesn't know about file formats, it never sends
FileContentsRequest. SocompletedFiles.sizenever reachesexpectedFileCount(seehandleFileContentsRequest()in this file), andonUploadFinishednever fires.monitoringSuppressedstays true permanently. - User copies text; the monitoring loop skips it. No
LocalClipboardChangedevent fires, noFormatListis sent to the server, deny text never appears again for the rest of the session.
The root issue is that onUploadStarted fires immediately and unconditionally, but onUploadFinished depends entirely on remote behavior (the remote requesting every single file). Under any blocking policy, or if the remote user simply never pastes, monitoring is suppressed forever.
Possible solutions:
- Timeout: Resume monitoring if no
FileContentsRequestarrives within N seconds afterinitiateFileCopy(), bounding the suppression window. - Short debounce instead of indefinite suppression: Suppress for a brief period (e.g., 500ms) after
initiateFileCopy()to prevent the polling loop from racing with theFormatListsend, then resume regardless of transfer state. TheuploadStatecan remain active to handle incomingFileContentsRequestPDUs independently. - Resume immediately after
initiateFileCopy()succeeds: The concern this suppression addresses is the monitoring loop clobbering the fileFormatListwith a text/image update. That race window is very short; only the time betweenonUploadStartedand theFormatListactually being sent on the wire. Resuming right afterinitiateFileCopy()returns (or after a small delay) would close the race without coupling monitoring lifetime to transfer completion.
There was a problem hiding this comment.
Fixed. Monitoring now resumes immediately after initiateFileCopy() (in a finally block), rather than waiting for all file contents requests to complete. The suppression window is intentionally brief - just long enough to prevent the clipboard polling loop from racing with the FormatList send. All scattered onUploadFinished calls in upload completion/error paths have been removed. Upload state tracking continues independently for progress and error reporting.
bbd1bb4 to
ecdc7d6
Compare
ecdc7d6 to
03b948e
Compare
|
Rebased on current upstream master (up to 1eaf333) to pick up recent changes and get CI running. All review items from CBenoit and ymarcus93 have been addressed - see individual thread replies above. |
Implement bidirectional file transfer over the CLIPRDR channel per MS-RDPECLIP, with proactive lock management, automatic timeout-based cleanup, and a protocol-agnostic provider interface. Rust (ironrdp-cliprdr): - File transfer state machine: FormatList with FileGroupDescriptorW, FileContentsRequest/Response for size and range queries - Proactive clipboard locking: automatic LockClipData on file detection, activity-tracked expiry via drive_timeouts() - Path sanitization: traversal stripping, UNC/drive prefix removal - CB_HUGE_FILE_SUPPORT_ENABLED validation for large file positions - Comprehensive test suite in ironrdp-testsuite-core (15 test modules) - Fuzz target for CLIPRDR channel processing Rust (iron-remote-desktop, ironrdp-web): - File transfer routed through extension mechanism (invoke_extension / extension) to keep iron-remote-desktop protocol-agnostic - Six builder-time callback extensions and three runtime operation extensions dispatched via extension_match! TypeScript (iron-remote-desktop): - FileTransferProvider interface for dependency injection - enableFileTransfer() accepts protocol-specific providers - Clipboard monitoring suppression composed via provider hooks TypeScript (iron-remote-desktop-rdp): - RdpFileTransferProvider: download/upload manager with event system, drag-and-drop support, directory traversal, chunked transfers - Extension factory functions for all file transfer callbacks and operations - 48 unit tests with mocked extension layer FFI (.NET): - FileContentsRequest/Response types for native clipboard backends - ClipboardMessage variants for file transfer PDU routing
03b948e to
c994ef2
Compare
|
Lockfile issues, hopefully fixed |
Add end-to-end clipboard file transfer (upload and download) across the CLIPRDR channel per MS-RDPECLIP. This spans the protocol layer, WASM/web backend, FFI bindings, and adds a TypeScript FileTransferManager.
This is a big PR. I've squashed the history because it was... a journey.
Key changes:
I suggest you start with
crates/ironrdp-cliprdr/README.md- it gives a complete overview of the design, API, and lock lifecycle.Some other review entry points that might help:
There are a bunch of breaking changes here. I can summarize if need be. ironrdp-cliprdr, -ffi and web stuff will want new releases.
You probably want tests in the test crate, but the cliprdr tests refer to private structures so that was going to be hairy. I do see some inline tests throughout the project. Let me know what you'd like moved.
This code is working well with our internal browser RDP product. I hope you like it.