125 lines
7.1 KiB
Markdown
125 lines
7.1 KiB
Markdown
# sftp-rs: Overview and Architecture
|
|
|
|
**Version**: 0.3.0
|
|
**Repository**: https://github.com/jelmer/sftp-rs
|
|
**License**: Apache-2.0
|
|
**Rust Edition**: 2021
|
|
|
|
## What It Is
|
|
|
|
`sftp-rs` is a Rust crate implementing an **SFTP client** — the SSH File Transfer Protocol as defined in [draft-ietf-secsh-filexfer-02](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02). It provides a pure wire-protocol codec plus both synchronous and asynchronous client implementations that layer on top of any transport providing `Read + Write` (sync) or `AsyncRead + AsyncWrite` (async) byte streams.
|
|
|
|
Core design decisions:
|
|
- **Transport-agnostic** — does not include an SSH implementation itself; operates on top of an already-established SSH channel or any byte stream
|
|
- **Protocol v3** — targets SFTP protocol version 3, following the published draft but deviating where other servers and clients ignore the RFC
|
|
- **Pure codec** — the `protocol` module contains zero I/O; it builds and parses raw bytes, shared by both sync and async clients
|
|
- **Two concurrency models** — a synchronous `SftpClient<C>` and an async `AsyncSftpClient<W>` with a background reader task for concurrent pipelining
|
|
|
|
## Source Layout
|
|
|
|
```
|
|
sftp-rs/src/
|
|
├── lib.rs # Crate root, re-exports, feature gating
|
|
├── protocol.rs # Pure wire-protocol codec: types, builders, parsers
|
|
├── sync.rs # Synchronous SftpClient<C: Read + Write>
|
|
├── async.rs # AsyncSftpClient<W: AsyncWrite + Unpin> with background reader
|
|
├── russh.rs # russh transport glue (optional, feature-gated)
|
|
└── bin/
|
|
└── sftp.rs # CLI interactive sftp client binary
|
|
```
|
|
|
|
## Feature Flags
|
|
|
|
| Feature | Default | Dependencies | Description |
|
|
|---------|---------|-------------|-------------|
|
|
| `default` | ✅ | `bin` | Includes the CLI binary |
|
|
| `bin` | ✅ (via default) | `rustyline`, `shell-words` | Interactive CLI binary |
|
|
| `ssh2` | ❌ | `ssh2` | Integration with the `ssh2` crate (libssh2 bindings) |
|
|
| `async` | ❌ | `tokio` | Async client (`AsyncSftpClient`) |
|
|
| `russh` | ❌ | `russh`, `async`, `tokio` | russh transport integration |
|
|
|
|
The `russh` feature implies `async` (it requires `tokio` and the async client).
|
|
|
|
## Key Dependencies
|
|
|
|
| Dependency | Version | Purpose |
|
|
|------------|---------|---------|
|
|
| `byteorder` | 1 | Big-endian binary read/write for wire protocol |
|
|
| `russh` | 0.61 (optional) | Pure-Rust SSH implementation, provides channel + stream |
|
|
| `ssh2` | 0.9 (optional) | libssh2 bindings, provides `Channel` |
|
|
| `tokio` | 1 (optional) | Async runtime, `AsyncRead`/`AsyncWrite`, `io::split` |
|
|
| `rustyline` | 18 (optional) | Readline library for CLI binary |
|
|
| `shell-words` | 1 (optional) | Shell-style token parsing for CLI |
|
|
|
|
## Architecture Diagram
|
|
|
|
```
|
|
┌───────────────────────────────────────────────────────────────┐
|
|
│ Application Layer │
|
|
│ │
|
|
│ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
|
|
│ │ CLI Binary │ │ SftpClient<C> │ │ AsyncSftp- │ │
|
|
│ │ (bin/sftp) │ │ (sync.rs) │ │ Client<W> │ │
|
|
│ │ │ │ │ │ (async.rs) │ │
|
|
│ │ SshChannel → │ │ C: Read+Write │ │ W: AsyncW │ │
|
|
│ │ SftpClient │ │ │ │ │ │
|
|
│ └────────┬─────────┘ └────────┬─────────┘ └──────┬──────┘ │
|
|
│ │ │ │ │
|
|
│ └──────────────────────┴────────────────────┘ │
|
|
│ │ │
|
|
│ ┌─────────────┴─────────────┐ │
|
|
│ │ protocol.rs (codec) │ │
|
|
│ │ │ │
|
|
│ │ • Error, Result │ │
|
|
│ │ • Attributes (serde) │ │
|
|
│ │ • OpenOptions │ │
|
|
│ │ • build_*() builders │ │
|
|
│ │ • parse_*() / expect_*() │ │
|
|
│ │ • read/write_raw_packet │ │
|
|
│ │ • with/split_request_id │ │
|
|
│ └────────────────────────────┘ │
|
|
│ │ │
|
|
└──────────────────────────────────┼──────────────────────────────┘
|
|
│
|
|
┌────────────────────┼────────────────────┐
|
|
│ │ │
|
|
┌─────────┴──────┐ ┌─────────┴───────┐ ┌─────────┴──────┐
|
|
│ ssh subprocess│ │ russh Channel │ │ ssh2 Channel │
|
|
│ (stdin/stdout)│ │ (via Channel- │ │ (libssh2) │
|
|
│ │ │ Stream) │ │ │
|
|
└────────────────┘ └────────────────┘ └────────────────┘
|
|
```
|
|
|
|
## Protocol Version Negotiation
|
|
|
|
Both clients perform the same handshake on construction:
|
|
|
|
1. **Client sends `SSH_FXP_INIT`** with version `3`
|
|
2. **Server responds `SSH_FXP_VERSION`** with its version and optional extensions
|
|
3. If server version ≠ 3, the constructor returns an error
|
|
|
|
```rust
|
|
// From protocol.rs
|
|
pub fn build_init() -> Vec<u8> {
|
|
let mut buf = Vec::with_capacity(4);
|
|
buf.write_u32::<BigEndian>(3).unwrap(); // version = 3
|
|
buf
|
|
}
|
|
```
|
|
|
|
The handshake is the only unnumbered (no request-id) exchange. All subsequent requests include a 4-byte request-id used for demultiplexing responses.
|
|
|
|
## Re-exports from `lib.rs`
|
|
|
|
The crate root re-exports the most commonly used types:
|
|
|
|
```rust
|
|
pub use protocol::{
|
|
Attributes, Directory, Error, File, Kind, OpenOptions, Result, TextHint,
|
|
SSH_FILEXFER_ATTR_ACCESSTIME, SSH_FILEXFER_ATTR_ACL, /* ... all flag constants */
|
|
};
|
|
pub use sync::SftpClient;
|
|
|
|
#[cfg(feature = "async")]
|
|
pub use r#async::AsyncSftpClient;
|
|
``` |