Skip to content

Commit

Permalink
Merge pull request #5 from AspectUnk/impl-client
Browse files Browse the repository at this point in the history
Implementation for client side
  • Loading branch information
AspectUnk authored Sep 16, 2023
2 parents 1608266 + 91d75ca commit 3da05bc
Show file tree
Hide file tree
Showing 26 changed files with 1,957 additions and 120 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ keywords = ["russh", "sftp", "ssh2", "server", "client"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tokio = { version = "1", default-features = false, features = ["io-util", "rt", "sync"] }
tokio = { version = "1", default-features = false, features = ["io-util", "rt", "sync", "time", "macros"] }
serde = { version = "1.0", features = ["derive"] }
bitflags = { version = "2.4", features = ["serde"] }

async-trait = "0.1"
thiserror = "1.0"
chrono = "0.4"
bytes = "1.4"
bytes = "1.5"
log = "0.4"

[dev-dependencies]
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# Russh SFTP
SFTP subsystem supported server and client for [Russh](https://github.com/warp-tech/russh)\
Implemented according to [version 3 specifications](https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt) (most popular)
SFTP subsystem supported server and client for [Russh](https://github.com/warp-tech/russh) and more!\
Crate can provide compatibility with anything that can provide the raw data stream in and out of the subsystem channel.\
Implemented according to [version 3 specifications](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02) (most popular).

## Examples
- [Client example](https://github.com/AspectUnk/russh-sftp/blob/master/examples/client.rs)
- [Simple server](https://github.com/AspectUnk/russh-sftp/blob/master/examples/server.rs)
- ~~Fully implemented server~~
- ~~Client example~~

## What's ready?
- [x] Basic packets
- [x] Extended packets
- [x] Simplification for file attributes
- [x] Client side
- [x] Client example
- [x] Server side
- [x] Simple server example
- [ ] Error handler (unlike specification)
- [x] Extension support: `[email protected]`, `[email protected]`
- [ ] Full server example
- [ ] Unit tests
- [ ] Workflow
- [ ] Client side
- [ ] Client example

## Some words
Thanks to [@Eugeny](https://github.com/Eugeny) (author of the [Russh](https://github.com/warp-tech/russh)) for his prompt help and finalization of Russh API
80 changes: 71 additions & 9 deletions examples/client.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use async_trait::async_trait;
use bytes::Bytes;
use log::{error, info, LevelFilter};
use russh::*;
use russh_keys::*;
use russh_sftp::client::SftpSession;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};

struct Client;

Expand All @@ -15,7 +16,7 @@ impl client::Handler for Client {
self,
server_public_key: &key::PublicKey,
) -> Result<(Self, bool), Self::Error> {
println!("check_server_key: {:?}", server_public_key);
info!("check_server_key: {:?}", server_public_key);
Ok((self, true))
}

Expand All @@ -25,16 +26,77 @@ impl client::Handler for Client {
data: &[u8],
session: client::Session,
) -> Result<(Self, client::Session), Self::Error> {
println!(
"data on channel {:?}: {:?}",
channel,
std::str::from_utf8(data)
);
info!("data on channel {:?}: {}", channel, data.len());
Ok((self, session))
}
}

#[tokio::main]
async fn main() {

env_logger::builder()
.filter_level(LevelFilter::Debug)
.init();

let config = russh::client::Config::default();
let sh = Client {};
let mut session = russh::client::connect(Arc::new(config), ("localhost", 22), sh)
.await
.unwrap();
if session.authenticate_password("root", "pass").await.unwrap() {
let mut channel = session.channel_open_session().await.unwrap();
channel.request_subsystem(true, "sftp").await.unwrap();
let sftp = SftpSession::new(channel.into_stream()).await.unwrap();
info!("current path: {:?}", sftp.canonicalize(".").await.unwrap());

// create dir and symlink
let path = "./some_kind_of_dir";
let symlink = "./symlink";

sftp.create_dir(path).await.unwrap();
sftp.symlink(path, symlink).await.unwrap();

info!("dir info: {:?}", sftp.metadata(path).await.unwrap());
info!(
"symlink info: {:?}",
sftp.symlink_metadata(path).await.unwrap()
);

// scanning directory
for entry in sftp.read_dir(".").await.unwrap() {
info!("file in directory: {:?}", entry.file_name());
}

sftp.remove_file(symlink).await.unwrap();
sftp.remove_dir(path).await.unwrap();

// interaction with i/o
let filename = "test_new.txt";
let mut file = sftp.create(filename).await.unwrap();
info!("metadata by handle: {:?}", file.metadata().await.unwrap());

file.write_all(b"magic text").await.unwrap();
info!("flush: {:?}", file.flush().await); // or file.sync_all()
info!(
"current cursor position: {:?}",
file.stream_position().await
);

let mut str = String::new();

file.rewind().await.unwrap();
file.read_to_string(&mut str).await.unwrap();
file.rewind().await.unwrap();

info!(
"our magical contents: {}, after rewind: {:?}",
str,
file.stream_position().await
);

file.shutdown().await.unwrap();
sftp.remove_file(filename).await.unwrap();

// should fail because handle was closed
error!("should fail: {:?}", file.read_u8().await);
}
}
10 changes: 5 additions & 5 deletions src/buf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ pub trait TryBuf: Buf {
impl<T: Buf> TryBuf for T {
fn try_get_u8(&mut self) -> Result<u8, Error> {
if self.remaining() < size_of::<u8>() {
return Err(Error::BadMessage);
return Err(Error::BadMessage("no remaining for u8".to_owned()));
}

Ok(self.get_u8())
}

fn try_get_u32(&mut self) -> Result<u32, Error> {
if self.remaining() < size_of::<u32>() {
return Err(Error::BadMessage);
return Err(Error::BadMessage("no remaining for u32".to_owned()));
}

Ok(self.get_u32())
}

fn try_get_u64(&mut self) -> Result<u64, Error> {
if self.remaining() < size_of::<u64>() {
return Err(Error::BadMessage);
return Err(Error::BadMessage("no remaining for u64".to_owned()));
}

Ok(self.get_u64())
Expand All @@ -40,15 +40,15 @@ impl<T: Buf> TryBuf for T {
fn try_get_bytes(&mut self) -> Result<Vec<u8>, Error> {
let len = self.try_get_u32()? as usize;
if self.remaining() < len {
return Err(Error::BadMessage);
return Err(Error::BadMessage("no remaining for vec".to_owned()));
}

Ok(self.copy_to_bytes(len).to_vec())
}

fn try_get_string(&mut self) -> Result<String, Error> {
let bytes = self.try_get_bytes()?;
String::from_utf8(bytes).map_err(|_| Error::BadMessage)
String::from_utf8(bytes).map_err(|_| Error::BadMessage("unable to parse str".to_owned()))
}
}

Expand Down
67 changes: 67 additions & 0 deletions src/client/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::io;
use thiserror::Error;
use tokio::sync::mpsc::error::SendError as MpscSendError;
use tokio::sync::oneshot::error::RecvError as OneshotRecvError;
use tokio::time::error::Elapsed as TimeElapsed;

use crate::error;
use crate::protocol::Status;

/// Enum for client errors
#[derive(Debug, Clone, Error)]
pub enum Error {
/// Contains an error status packet
#[error("{}: {}", .0.status_code, .0.error_message)]
Status(Status),
/// Any errors related to I/O
#[error("I/O: {0}")]
IO(String),
/// Time limit for receiving response packet exceeded
#[error("Timeout")]
Timeout,
/// Occurs due to exceeding the limits set by the `[email protected]` extension
#[error("Limit exceeded: {0}")]
Limited(String),
/// Occurs when an unexpected packet is sent
#[error("Unexpected packet")]
UnexpectedPacket,
/// Occurs when unexpected server behavior differs from the protocol specifition
#[error("{0}")]
UnexpectedBehavior(String),
}

impl From<Status> for Error {
fn from(status: Status) -> Self {
Self::Status(status)
}
}

impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::IO(error.to_string())
}
}

impl<T> From<MpscSendError<T>> for Error {
fn from(err: MpscSendError<T>) -> Self {
Self::UnexpectedBehavior(format!("SendError: {}", err))
}
}

impl From<OneshotRecvError> for Error {
fn from(err: OneshotRecvError) -> Self {
Self::UnexpectedBehavior(format!("RecvError: {}", err))
}
}

impl From<TimeElapsed> for Error {
fn from(_: TimeElapsed) -> Self {
Self::Timeout
}
}

impl From<error::Error> for Error {
fn from(error: error::Error) -> Self {
Self::UnexpectedBehavior(error.to_string())
}
}
48 changes: 48 additions & 0 deletions src/client/fs/dir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use std::collections::VecDeque;

use super::Metadata;
use crate::protocol::FileType;

/// Entries returned by the [`ReadDir`] iterator.
#[derive(Debug)]
pub struct DirEntry {
file: String,
metadata: Metadata,
}

impl DirEntry {
/// Returns the file name for the file that this entry points at.
pub fn file_name(&self) -> String {
self.file.to_owned()
}

/// Returns the file type for the file that this entry points at.
pub fn file_type(&self) -> FileType {
self.metadata.file_type()
}

/// Returns the metadata for the file that this entry points at.
pub fn metadata(&self) -> Metadata {
self.metadata.to_owned()
}
}

/// Iterator over the entries in a remote directory.
pub struct ReadDir {
pub(crate) entries: VecDeque<(String, Metadata)>,
}

impl Iterator for ReadDir {
type Item = DirEntry;

fn next(&mut self) -> Option<Self::Item> {
match self.entries.pop_front() {
None => None,
Some(entry) if entry.0 == "." || entry.0 == ".." => self.next(),
Some(entry) => Some(DirEntry {
file: entry.0,
metadata: entry.1,
}),
}
}
}
Loading

0 comments on commit 3da05bc

Please sign in to comment.