Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion crates/ironrdp-activex/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,10 @@ async fn handle_request(shared: &Arc<Shared>, dispatcher: isize, request: Reques
| Request::ClipboardGetImage
| Request::ClipboardSetImage { .. }
| Request::ClipboardGetHtml
| Request::ClipboardSetHtml { .. } => Response::typed_error(
| Request::ClipboardSetHtml { .. }
| Request::ClipboardSetFiles { .. }
| Request::ClipboardListFiles
| Request::ClipboardGetFile { .. } => Response::typed_error(
AgentErrorCategory::Unavailable,
"clipboard access is unavailable through ActiveX",
),
Expand Down
77 changes: 77 additions & 0 deletions crates/ironrdp-agent/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ enum Command {
#[arg(long, value_parser = parse_clipboard_html)]
html: String,
},
/// Offer local files to the remote via the clipboard file-list mechanism. Each path names a
/// single regular file; a directory is rejected.
ClipboardSetFiles(ClipboardSetFilesArgs),
/// List the remote's currently offered files, if any (metadata only; nothing is fetched).
ClipboardListFiles,
/// Fetch one file's full contents from the remote by its position in the last
/// `clipboard-list-files` listing.
ClipboardGetFile(ClipboardGetFileArgs),
/// Send one MS-RDPEI touch contact sample (legal flag sets only).
Touch {
#[arg(long, default_value_t = 0)]
Expand Down Expand Up @@ -613,6 +621,24 @@ struct ClipboardSetImageArgs {
path: PathBuf,
}

#[derive(Args, Debug)]
struct ClipboardSetFilesArgs {
/// One or more local file paths to offer. A directory is rejected; folders are not supported.
#[arg(required = true)]
paths: Vec<PathBuf>,
}

#[derive(Args, Debug)]
struct ClipboardGetFileArgs {
/// Position in the last `clipboard-list-files` listing.
index: i32,
/// Destination path. Required rather than defaulted from the remote's own file name, which
/// is untrusted input (the same reasoning `ironrdp_cliprdr`'s own docs give for not writing
/// a remote-supplied name straight to disk).
#[arg(long = "out")]
out: PathBuf,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum CliMouseButton {
Left,
Expand Down Expand Up @@ -991,6 +1017,28 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> {
}
Request::ClipboardSetImage { png }
}
Command::ClipboardSetFiles(args) => {
let paths = args
.paths
.into_iter()
.map(|path| path.to_string_lossy().into_owned())
.collect();
Request::ClipboardSetFiles { paths }
}
Command::ClipboardListFiles => Request::ClipboardListFiles,
Command::ClipboardGetFile(args) => {
let response = transport::send_request(&endpoint, &Request::ClipboardGetFile { index: args.index }).await?;
let payload = match response {
Response::Ok(payload) => payload,
Response::Err(message) => anyhow::bail!("{message}"),
};
let Payload::ClipboardFile(data) = payload else {
anyhow::bail!("unexpected response to clipboard-get-file request");
};
std::fs::write(&args.out, &data).with_context(|| format!("write {}", args.out.display()))?;
println!("wrote {} ({} bytes)", args.out.display(), data.len());
return Ok(());
}
Command::MouseMove { x, y } => Request::MouseMove { x, y },
Command::MouseButton { button, pressed } => Request::MouseButton {
button: button.into_button(),
Expand Down Expand Up @@ -2180,6 +2228,35 @@ fn print_payload(payload: Payload) {
},
// Handled out-of-band by the `ClipboardGetImage` command, never printed here.
Payload::ClipboardImage(png) => println!("clipboard image ({} bytes)", png.as_ref().map_or(0, Vec::len)),
Payload::ClipboardFileList(files) => match files {
Some(files) if files.is_empty() => println!("(no files)"),
Some(files) => {
for (index, file) in files.iter().enumerate() {
// `name`/`relative_path` come from the remote peer. `CLIPRDR` sanitization
// removes path traversal and null bytes but not terminal control sequences;
// `escape_debug` keeps printable Unicode readable while escaping the ANSI/OSC
// control characters an unescaped print would otherwise pass straight to the
// terminal.
let path = match &file.relative_path {
Some(relative_path) => {
format!("{}\\{}", relative_path.escape_debug(), file.name.escape_debug())
}
None => file.name.escape_debug().to_string(),
};
let kind = if file.is_directory { "dir" } else { "file" };
let size = file.size.map_or_else(|| "?".to_owned(), |size| size.to_string());
// Unix seconds, not a formatted date: keeps this crate free of a date/time
// dependency for what is otherwise a plain integer field.
let mtime = file
.last_write_time
.map_or_else(|| "?".to_owned(), |time| time.to_string());
println!("{index}: {kind} {size:>12} {mtime:>10} {path}");
}
}
None => println!("(no files on the remote clipboard)"),
},
// Handled out-of-band by the `ClipboardGetFile` command, never printed here.
Payload::ClipboardFile(data) => println!("clipboard file ({} bytes)", data.len()),
}
}

Expand Down
32 changes: 26 additions & 6 deletions crates/ironrdp-agent/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,12 @@ Override with `--endpoint <PATH-OR-PIPE>` on any subcommand.

## Clipboard

Text (`CF_UNICODETEXT`), images (`CF_DIB`/`CF_DIBV5`, as PNG files), and HTML fragments
(`HTML Format`); no file transfer. Local content is a single logical item: setting one replaces
whatever was set before, regardless of kind. A remote copy is requested image over HTML over text,
richest representation first, when the remote offers more than one.
Text (`CF_UNICODETEXT`), images (`CF_DIB`/`CF_DIBV5`, as PNG files), HTML fragments
(`HTML Format`), and files (the `FileGroupDescriptorW` file-list mechanism); no folders. Local
content is a single logical item: setting one replaces whatever was set before, regardless of kind.
A remote copy is requested files over image over HTML over text, richest representation first,
when the remote offers more than one. File listing is metadata only; a file's contents are fetched
only on explicit request.

- `clipboard-get` Print the last text received from the remote clipboard, or
`(empty)` if none has arrived yet. Requires an active session.
Expand All @@ -172,10 +174,28 @@ richest representation first, when the remote offers more than one.
`clipboard-set`.
- `clipboard-get-html` Print the last HTML fragment received from the remote
clipboard, or `(empty)` if the current remote item isn't
HTML (nothing has arrived, or the last copy was text or an
image).
HTML (nothing has arrived, or the last copy was text, an
image, or files).
- `clipboard-set-html --html HTML` Set the local clipboard HTML fragment and advertise it to the
remote. Same before-connect behavior as `clipboard-set`.
- `clipboard-set-files PATH...` Offer one or more local files to the remote via the clipboard
file-list mechanism. Each path must be a regular file; a
directory is rejected outright, not skipped. Works before a
session connects too: the offer is stored and advertised as
soon as the clipboard channel initializes. Once a session is
active, it must have negotiated file transfer support, or the
call fails.
- `clipboard-list-files` List the remote's currently offered files (name, path within
the copied collection, size, last-write time as Unix seconds,
and whether it is a directory entry), or a no-files message if
none are offered. Nothing is downloaded; this only inspects
metadata already received.
- `clipboard-get-file INDEX --out PATH`
Fetch one file's full contents by its position in the last
`clipboard-list-files` listing and write it to `PATH`. Fails
cleanly on a directory entry, an out-of-range index, or a
file too large for the RPC transport, rather than attempt a
partial or corrupted download.

## NOW remote execution (requires an active, connected RDP session)

Expand Down
1 change: 1 addition & 0 deletions crates/ironrdp-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ironrdp-rail = { version = "0.1.0", path = "../ironrdp-rail" }
ironrdp-connector = { path = "../ironrdp-connector" }
ironrdp-session = { path = "../ironrdp-session" }
now-proto-pdu = { version = "0.4", features = ["std"] }
tokio = { version = "1", features = ["test-util"] }

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down
Loading
Loading