Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/herdr-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ crossbeam-channel.workspace = true
herdr-protocol.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
tempfile.workspace = true
thiserror.workspace = true
tracing.workspace = true
Expand Down
50 changes: 50 additions & 0 deletions crates/herdr-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,56 @@ arbitrary remote paths. The remote account and temporary-directory ancestors
must remain trusted; shell-based cleanup cannot eliminate concurrent path
replacement races by that account.

### One File In Place

```text
fetch_remote_file(file: RemoteFile, out: impl Write, max_bytes: u64,
cancelled: &AtomicBool) -> Result<Fetched>
push_remote_file(file: RemoteFile, expect: Expect, access: RemoteFileAccess,
len: u64, input: impl Read, cancelled: &AtomicBool) -> Result<()>
```

These copy one file at the host's own path off an SSH host and write it back,
for the GUI's remote file links. `RemoteFile` names the target, the host's
path as the host spells it (absolute, or `~/` for the login user's home), and
the `RemoteShell`: `Posix` runs `/bin/sh` scripts, and `PowerShell` runs
Windows PowerShell 5.1+ scripts through `-EncodedCommand` on Windows OpenSSH
hosts. The path travels quoted in the script; the file's bytes travel only on
stdout and stdin. Both are blocking background-worker APIs, on every client
OS, with the bridge's SSH trust policy and a 30-second no-progress timeout.

A fetch refuses a missing path, a folder or device, and a file over
`max_bytes` (`MAX_REMOTE_FILE_BYTES`, 64 MB, for the GUI) before streaming, and
returns the byte count and SHA-256 of what it wrote. A push writes a temporary
file beside the target and renames it over the target only when it holds
exactly `len` bytes and the host's file still meets `expect`: `Sha256` (the
file as fetched), `Absent`, or `Any`. A symlink or non-regular file is never
replaced. Another writer can still slip in between the check and the rename.

A push never lets anyone read the bytes who could not read the file they came
from, except as noted below. `access` says where a new file's access comes from: `Keep` (the login
umask's default on POSIX, the folder's inherited ACL on Windows) or
`Like(path)`, another host file's. An existing target always keeps its own.
On POSIX the temporary file is 0600 and takes the target's (or the `Like`
file's) group and mode before the rename. The target is written in place
instead when its group cannot be given and its group and other bits differ,
or when `ls` (`+`, or macOS's `@`) or `getfacl` shows an ACL on the target or
the temporary file. That keeps owner, group, ACL and hard links, but is not
atomic. A new `Like` file whose access cannot be copied (an ACL, a group that
cannot be given, a source that is not a plain file) stays 0600. On Windows
the temporary file is created with the target's (or the `Like` file's) DACL,
and `Replace` keeps it; a `Like` source whose DACL cannot be read, or that is
not a file, is refused.

Limitation: on a macOS host, an inheritable folder ACL entry can read the
temporary file while the upload is written, since macOS ACLs are not masked
by its 0600 mode. The final file is still right.

Refusals map to `RemoteFileError` variants (`Missing`, `NotAFile`,
`TooLarge`, `Symlink`, `Conflict`, `Incomplete`, `NoHasher`, and `Access`
when the file's access cannot be kept, before the host's file changes);
anything else is `Script`.

## Local Discovery

`Local` checks `HERDR_SOCKET_PATH` (derive `-client.sock` from its file stem),
Expand Down
2 changes: 2 additions & 0 deletions crates/herdr-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ pub enum Error {
source: Box<Error>,
cleanup: Box<Error>,
},
#[error(transparent)]
RemoteFile(#[from] crate::remote_file::Error),
#[error("host scripts require a Linux or macOS client")]
ScriptUnsupported,
#[error("could not start host script")]
Expand Down
5 changes: 5 additions & 0 deletions crates/herdr-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod method;
mod options;
mod process;
mod queue;
mod remote_file;
mod remote_host;
mod script;
pub mod scrollback;
Expand Down Expand Up @@ -68,6 +69,10 @@ pub use options::ConnectOptions;
#[cfg(windows)]
pub use process::CREATE_NO_WINDOW;
pub use process::new_command;
pub use remote_file::{
Access as RemoteFileAccess, Error as RemoteFileError, Expect, Fetched, MAX_REMOTE_FILE_BYTES,
RemoteFile, RemoteShell, fetch_remote_file, push_remote_file,
};
pub use remote_host::RemoteHost;
pub use script::{ScriptHost, ScriptLimits, run_script, run_script_command, shell_quote};
pub use sessions::{
Expand Down
Loading